プログラミング特論 (11) データ抽象化とモジュール 話題 ◆ モジュールプログラム開発 • 段階的詳細化 (step-wise refinement) • インタフェイス、仕様、および実装 ◆ 言語によるモジュール化支援 • 手続き抽象 (procedural abstraction) • 抽象データ型 櫻井彰人 – 表現の独立性 (representation independence) – データ型の帰納 (datatype induction) • パッケージとモジュール • 汎用抽象化 (generic abstractions) Course web site: http://www.sakurai.comp.ae.keio.ac.jp/ 段階的詳細化 (stepwise refinement) ◆ Wirth, 1971 • “…プログラム… は、一連の詳細化段階を経て、次第に 開発される” – 型パラメータをもつ関数やモジュール Dijkstra の例 (p.35) begin print first 1000 primes end – “… program ... gradually developed in a sequence of refinement steps” (Introduction より) (1969) begin variable table p fill table p with first 1000 • 各段階では、命令は … より詳細な命令に分解される – In each step, instructions … are decomposed into more detailed instructions. (Introduction より) primes begin int array p[1:1000] end print table p make for k from 1 to 1000 p[k] equal to k-th prime N. Wirth, Program Development by Stepwise Refinement, CACM, Vol. 14, No. 4, April 1971, pp. 221-227. プログラム構造 Sub-program •Dijkstra, E. W. Notes on structured programming. EWD 249 Technical U. Eindhoven, The Netherlands, 1969. データの詳細化 ◆ Wirth, 1971 再び: Main Program Sub-program end print p[k] for k from 1 to 1000 Sub-program Sub-program • 作業が詳細化されるとともに、データも同様に詳細化 され、 分解され、または構造化されるであろう. プロ グラム仕様とデータ仕様を並行して詳細化するのは 自然である。 – As tasks are refined, so the data may have to be refined, decomposed, or structured, and it is natural to refine program and data specifications in parallel. (Introduction より) Sub-program 1 例 モジュールプログラム設計 ◆ トップダウン設計 銀行取引 • 主たる作業を手始めに、次々と詳細化していく ◆ ボトムアップ設計 預金 引出し 印字 • 基礎概念を実装し、次にそれを結合 ◆ プロトタイピング ◆ レベル 2 のためには, 口座残高 を整数型変数で表す必要あり ◆ レベル 3 のためには, 過去の取 引リストを保持する必要がある • 全体システムの荒い近似を作成する • 次第に、機能を追加していく 取引履歴の印字 モジュール性 例: 関数部品 (function component) ◆ 部品 (component) ◆ 部品 • 意味のあるプログラム単位 – 機能、データ構造、モジュール, … ◆ インタフェイス • ある部品内で定義された型と操作のうち、部品の外から見えるもの ◆ 仕様 • 目論んだ部品の動作で、インタフェイスを通じて観測可能な性質とし て表現されたもの ◆ 実装 • 部品内のデータ構造と機能 例: データ型 ◆ 部品 • 優先順位付き待ち行列: 優先順位が減少する順序で要素を返すデ ータ構造 ◆ インタフェイス • 型 pq • 操作 empty : pq insert : elt * pq → pq deletemax : pq → elt * pq ◆ 仕様 • Insert は要素 elt を記憶した要素 pq に付け加える • Deletemax は優先順位最高の要素 elt と残りの要素の pq を返す • 平方根を計算する関数 ◆ インタフェイス • float sqroot (float x) ◆ 仕様 • If x>1, then sqrt(x)*sqrt(x) ≈ x. ◆ 実装 float sqroot (float x){ float y = x/2; float step=x/4; int i; for (i=0; i<20; i++){if ((y*y)<x) y=y+step; else y=y-step; step = step/2;} return y; } データ構造ライブラリを用いたヒープソート ◆ 優先順位つき待行列: 3つの操作をもつ構造 empty : pq insert : elt * pq → pq deletemax : pq → elt * pq ◆ 優先順位付き待行列を用いたアルゴリズム(ヒープソート) begin pq s を empty 配列から各要素を s に insert 減少順に要素を取り除き、配列におき戻す end これにより O(n log n) のソートアルゴリズムが得られる 2 情報隠蔽を支援する言語特徴 ◆ 手続き抽象化 • 手続きや関数のもつ機能性を隠蔽 ◆ データ抽象化 • データ構造の表現および操作の実装について決定したこ とを隠す • 例: 優先順位付き待ち行列は、二進探索木または部分ソ ート済み配列でありうる 手続き型言語では、手続きやデータの詳細化は、それらを書 換えることにより行う(隠すことはできない). 再利用については 、オブジェクト指向で. 抽象データ型 (abstract data types) ◆ 1970年代に目覚しく発展 ◆ 主たるアイデア: • インタフェイスを実装から分離する –例 • 集合のインタフェイスには、empty, insert, union, is_member?, … • 集合の実装には、 … リンクによるリスト (linked list) … • 型チェックを用いて、分離を強制する – クライアントプログラムは、インタフェイス中の操作にしかアクセス できない – 実装は, 抽象データ型の構成子のなかにカプセル化される 抽象データ型の源流 組み込み型 (built-in types) との比較 ◆ 構造化プログラミング、データ詳細化 ◆ 例: int • あるべき操作を仮定してプログラムを書く • 後に、この操作を実装する • 例: – ある記号表を仮定して、式の解析プログラムを書く – 後に、記号表データ構造を実装 ◆ 拡張可能言語に関する研究 • 組み込み型の本質的性質は何か? • 同様の性質をもったユーザ定義型を提供したい • 例: • この型の変数が宣言できる x: int • この型特有の組込み演算 +, -, *, … • 整数値に対しては、それ以外の演算は適用できない ◆ 類似の性質が、抽象データ型にも望ましい • 変数宣言ができる x : abstract_type • 演算の集合が定義できる (インタフェイスを与える) • 言語は、このabstract_type にはこの演算(操作)のみが 適用できることを保証する。 – 組み込みのリストと同じリスト型を定義する ML の Abstype 複素数の abstype ◆ 値と演算をもつ新しい型を宣言する ◆ 入力 abstype t = <tag> of <type> with val <pattern> = <body> ... fun f(<pattern>) = <body> ... end ◆ 表現 t = <tag> of <type> ML データ型の宣言と類似 abstype cmplx = C of real * real with fun cmplx(x,y: real) = C(x,y) fun x_coord(C(x,y)) = x fun y_coord(C(x,y)) = y fun add(C(x1,y1), C(x2,y2)) = C(x1+x2, y1+y2) end ◆ 型 (コンパイラ出力) type cmplx val cmplx = fn : real * real -> cmplx val x_coord = fn : cmplx -> real val y_coord = fn : cmplx -> real val add = fn : cmplx * cmplx -> cmplx 3 有限集合の abstype カプセル化の原則 ◆ 宣言 ◆ 表現独立 (Representation Independence) abstype 'a set = SET of 'a list with val empty = SET(nil) fun insert(x, SET(elts)) = ... fun union(SET(elts1), Set(elts2)) = ... fun isMember(x, SET(elts)) = ... end ◆型 (コンパイラ出力) type 'a set val empty = - : 'a set val insert = fn : 'a * ('a set) -> ('a set) val union = fn : ('a set) * ('a set) -> ('a set) val isMember = fn : 'a * ('a set) -> bool ◆ データ型帰納法 (Datatype Induction) • 抽象データ型に関する推論方法 • インタフェイスと実装とが分離されていることに基づく 現実か理想か? 表現独立 ◆ Integers • 0,1,2, …, -1,-2, … が表現できる限り、どんな方法でもよい • ただし、演算は正しく動作しないといけない +, -, *, /, print, … • 例 1の補数 v.s. 2の補数 ◆ Finite Sets • {x, y, z, … } が表現できる限り、どんな方法でもよい • ただし、演算は正しく動作しないといけない empty, ismember?, insert, union • 例 リンクによるリスト vs 二進木 vs ビット・ベクトル 帰納法 • 抽象データ型の各要素は、様々な方法で実装可能 • インタフェイスが制限される -> クライアントプログラムは ある 良い 実装を他の実装と区別することはできない (データ型帰納法へ向けて) ◆ 主たるアイデア • 0 は自然数である • もし x が自然数であれば, x+1 も自然数である • こうして得られるもののみが自然数である ◆ 全ての n について p(n) を証明せよ • p(0) を証明せよ • 「もし p(x) が正しいなら p(x+1) が正しい」を証明せよ • これだけができればよい ◆ Clu, ML, … においては、表現独立は定理 • 証明できる。というのも、言語が実装へのアクセスを制限 しているから. アクセスはインタフェイスを通じてのみ ◆ C, C++, においては、表現独立は理想 • “良いプログラミング・スタイル” に従えば表現独立性が 得られる • 言語はそれを強制しはしない 例: -1 のビット表現が印字可能 1の補数か2の補数かが分かる 整数リストに関する帰納法 ◆ 原理 • nil はリストである • もし y がリストで x が int であれば cons(x,y) はリストである • これがリストのすべてである ◆ p(y) を全てのリストy について証明する • p(nil) を証明する • 「もし p(y) なら p(cons(x,y)) である」を証明する • これだけがすべきこと ◆ 例: 次のスライド • 注: car や cdr を考える必要はない • なぜ? 新リストなし. (整数の帰納法に引き算がないのと同様.) 4 リストを用いた帰納法の例 ◆ リストをソートする関数 • fun sort(nil) = nil • | sort(x::xs) = insert(x, sort(xs)) ◆ ソート済みリストへの挿入 • fun insert(x, nil) = [x] • | insert(x, y::ys) = if x<y then x::(y::ys) • else y::insert(x,ys) データ型帰納法のためのインタフェイス ◆ データに対する操作を分類 • 構成子(constructors): そのデータ型の要素を作成 • 演算子(operators): 要素を結合, しかし「新」要素生成せず • 観測子(observers): 他の型の値を生成 ◆ 例: • 集合 ◆ これらの関数の正しさを証明せよ • リスト上の帰納法を使用 (易しい、というのも ML が我々 にそのように書かせているから) empty : set insert : elt * set -> set union : set * set -> set isMember : elt * set -> bool • 分類 構成子: empty, insert 演算子: union 構成子上の帰納法 集合上の帰納の例 ◆ 演算子: 新要素は生成しない ◆ 仮定: map 関数 • 例: 有限集合上の union union を使って定義されたどんな集合も union なしに定義できる: union(empty, s) = s union(insert(x,y), s) = insert(x, union(y,s)) ◆ 性質は帰納法によって証明 • 構成子によって生成される全ての要素について示す 集合の例: 「 P(empty) と P(y) => P(insert(x,y)) 」を証明 観測子: isMember • map(f,empty) = empty • map(f, insert(y,s)) = union(f(y), map(f,s)) ◆ 集合の共通部分を求める関数 • fun intersect(s,s’) = if empty(s’) then s’ else let f(x) = if member(x,s) then {x} else empty in map(f, s’) end; ◆ これでよいことを証明せよ: • s’ 上の帰納を使用: – 正しい、もし s’ = empty – 正しい、もし s’ = insert(y, s’’) • これはこの型のすべての要素をカバーする 以上の帰納法のポイントは? モジュール ◆ データ抽象は詳細を隠蔽 ◆ 抽象データ型を使用するプログラムに関する推論 は、抽象的に行える ◆ 情報隠蔽のための一般的な構成要素 ◆ 2個の部分からなる • データ型の基礎的な性質を使用 • データ型がどう実装されたかは無視 • インタフェイス (interface): 名前とそれらの型の集合 • 実装 (implementation): インタフェイス中の要素すべての宣言 隠蔽された要素に対する追加の宣言 ◆ 例: • Modula modules, Ada packages, ML structures, ... 5 モジュールとデータ抽象 総称的抽象化 (generic abstractions) module Set interface type set val empty : set fun insert : elt * set -> set fun union : set * set -> set fun isMember : elt * set -> bool implementation type set = elt list val empty = nil fun insert(x, elts) = ... fun union(…) = ... ... end Set ◆ 型や他モジュールでパラメータ化したモジュール ◆ 汎用的な実装が可能となる 抽象データ型が定義可能 Private な型 Public な操作 より一般的 関連した型と操作をい くつか ある言語では インタフェイスと実装を 分離 一つのインタフェイスで 複数の実装が可能 • 多くの方法で実体化する(instantiate)ことができる ◆ 言語の例: • Ada generic packages, C++ templates, ML functors, … • C++ Standard Template Library (STL) には、広範囲の 例がある C++ Templates 例 ◆ 型パラメータ化機構 ◆ 単相型 (monomorphic) swap 関数 • template<class T> … が型パラメータ T を示す • C++ にはクラステンプレートと関数テンプレートがある – 関数の場合をこれから見る ◆ リンク時の実体化 (instantiation) • 個々の型ごとに、テンプレートの実体が別々に作られる • コードを重複させるのはなぜ? – 起動記録内の局所変数の大きさが異なる – パラメータの型ごとに異なる演算へリンクする 総称的な sort 関数 ◆ 関数にはパラメータの型に対応した < が必要 template <class T> void sort( int count, T * A[count] ) { for (int i=0; i<count-1; i++) for (int j=I+1; j<count-1; j++) if (A[j] < A[i]) swap(A[i],A[j]); } ◆ 関数 < はどうやって見つけるのか? • sort 関数と呼出側プログラムとをリンクする • リンク時に実際の T が決まる • もし < が T 上で定義されていば OK, さもなくばエラー void swap(int& x, int& y){ int tmp = x; x = y; y = tmp; } ◆ 多態型 (polymorphic) 関数テンプレート template<class T> void swap(T& x, T& y){ T tmp = x; x = y; y = tmp; } ◆ 呼出しは普通の関数のように float a, b; … ; swap(a,b); … ML の多態型との比較 fun insert(less, x, nil) = [x] | insert(less, x, y::ys) = if less(x,y) then x::y::ys else y::insert(less,x,ys) fun sort(less, nil) = nil | sort(less, x::xs) = insert(less, x, sort(less,xs)) ◆ 多層型 (polymorphic) sort 関数 • 演算を関数として渡す • 実体化はなし. 全てのリストは同じ方法 (Lisp のように cons セルを 用いて)で表現されているから. ◆ 一様なデータ表現 • コードは小さく, 効率は落ちるかもしれないが, リンクは複雑ではない – オーバーローディングを解決する必要もあるであろう, etc. 6 C++ Standard Template Library STL における主たる要素 ◆ 多くの総称的抽象化 ◆ コンテナ(container): 型付きオブジェクトの集合 • 多態的な抽象データ型と演算 ◆ 多くの目的に有用 • 優れた例 generic programming ◆ 実行時間は効率的 (しかし空間についてはいつもではない) ◆ 記述は C++ • テンプレート機構とオーバーローディングをしよう • オブジェクトには依存せず アーキテクト: Alex Stepanov • 例: array, list, associative dictionary, ... ◆ イタレータ(iterator): ポインタや番地の一般化 ◆ アルゴリズム ◆ アダプタ (adapter): ある形から別の形への変換 • 例: 更新可能なコンテナからイタレータを作る ◆ 関数オブジェクト: 閉包 (“手作り”) ◆ アロケータ (allocator): カプセル化したメモリプール • 例: GC 付きメモリ, 参照カウント付きメモリ, ... STL を用いた例 STL でのマージ ◆ 2個のソート済みリストのマージ ◆ 範囲(range)はイタレータにより表現 • merge : range(s) × range(t) × comparison(u) → range(u) 概念的には正しいが, STL 構文に沿っていない. ◆ 使用される基本概念 • range(s) – 型 s の順序付き “list”, 先頭と最終要素への ポインタで与えられる • comparison(u) – 型 u の上のブール値関数 • subtyping - s と t は u の部分型でなければならない • イタレータ (iterator) はポインタの一般化 • ++ (次の要素へ動く) のサポート ◆ 比較 (comparison) 演算子は Compare クラスのオ ブジェクト ◆ テンプレートを用いた多態型 (polymorphism) template < class InputIterator1, class InputIterator2, class OutputIterator, class Compare > OutputIterator merge(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, InputIterator1 last2, OutputIterator result, Compare comp) STL と他のライブライとの比較 STL の効率性 ◆ C: ◆ sort の実行時間 qsort( (void*)v, N, sizeof(v[0]), compare_int ); ◆ C++, ただし、元々の C 配列を用いる: int v[N]; sort( v, v+N ); ◆ C++, ただし、vector クラスを用いる: vector v(N); sort( v.begin(), v.end() ); C C++ (raw arrays) C++ (vector class) N = 50000 1.4215 0.2895 0.2735 N = 500000 18.166 3.844 3.802 ◆ ポイント • 総称的な抽象化は、便利で効率的なりうる ! • しかし、コードサイズには気をつけよ、もし C++ テンプレ ートを用いるなら… 7
© Copyright 2024 ExpyDoc