われわれは今や数学的抽象の決定的段階に来た. われわれは記号が何を意味するかを忘れる. ... [数学者は]怠惰である必要はない. それらが代表するものを見なければならないこともなしに, これらの記号を使って彼らが実行出来る操作はたくさんある.
Hermann Weyl 思考の数学的方法
本章では, 更に複雑なデータを見ていこうと思う. 1章の手続きはどれも単純な数値データを扱ったが, 単純なデータは計算を使って立ち向おうとすると多くの問題には不向きである. プログラムは主として複雑な現象をモデル化するために設計する. そして多くの場合, 複数の様相を持つ実世界の現象をモデル化するため, 複数の部品を持つ計算オブジェクトを構成しなければならない. そこで, 1章では手続きを組み合せ, 合成手続きを作って抽象を構築してきたが, 本章では, プログラム言語のもう一つの重要な側面---データオブジェクトを組み合せ, 合成データ(compound data)を作って抽象を構築する機能---を扱う.
プログラム言語に合成データが欲しいのはなぜか. 合成手続きが欲しかったのと同じ理由である: プログラム設計の思考レベルを高めるため, 設計の部品化力を増やすため, プログラム言語の表現力を広めるためである. 手続きが定義出来ると, 言語の持つ基本的演算より高いレベルの思考でプロセスを扱うことが出来たのと同様に, 合成データオブジェクトが作れると, 言語の持つ基本的データオブジェクトより高いレベルの思考でデータを扱うことが出来る.
有理数で算術演算をするシステムを設計する作業を考えよう. 有理数を二つとり,和を作る演算add-ratが考えられる. 単純なデータとしては, 有理数を二つの整数: 分子と分母と思うことが出来る. そこで有理数が二つの整数(分子と分母)で表現され, add-ratは二つの手続き(一つは和の分子を作り, もう一つは分母を作る)で実装されるプログラムを設計することは出来る. しかし, どの分子がどの分母に対応するかしっかり覚えておかなければならず, これは煩わしい. 多くの有理数に多くの演算を施そうとするシステムでは, かような詳細な記録づけはプログラムを実質的に混乱させ, 何がしたかったのか, われわれに語り掛けない. 分子と分母を「糊づけ」にし, 有理数を一個の思考単位と見ることで, プログラムを一貫した方法で操作出来るような対--- 合成データオブジェクト(compound data object)---を作ることが出来たら, 遥かによいであろう.
合成データを使うと, プログラムの部品化度を増やすことが出来る. 有理数をオブジェクトとして直接扱えるなら, 有理数を整数の対としてどう表現するかというような細部から, プログラムの有理数自身を扱う部分を分離することが出来る. データオブジェクトをどう表現するかに関するプログラムの部分を, データオブジェクトをどう使うかに関するプログラムの部分から隔離する一般的技法は データ抽象(data abstraction)という強力な設計手法である. データ抽象がプログラムの設計, 維持, 修正を容易にすることを見ていこう.
合成データを使うと, プログラム言語の表現力が本当に大きくなる. 「線形結合」ax + byを作ることを考えてみよう. a, b, xとyを引数としてとり, ax + byの値を返す手続きが書きたいとする. 引数が数値なら何でもない. すぐに手続き
(define (linear-combination a b x y) (+ (* a x) (* b y)))が定義出来る. しかし, 数値だけに関心があるのではないとしよう. 加算と乗算が定義されるなら, 有理数, 複素数, 多項式その他の線形結合が出来ることを, 手続き的に表したかったとしよう. これを手続きとして
(define (linear-combination a b x y) (add (mul a x) (mul b y)))の形で表せるであろう. addやmulは+や*の基本的手続きではなく, 引数a, b, xやyに渡したデータの種類に見合った演算を実行するもっと複雑なものである. 重要なことはlinear-combinationがa, b, xおよびyについて知らなければならないのは手続きaddやmulが適切な操作を実行することだけである. 手続きlinear-combinationの視点からは, a, b, xやyが何であるかには無関係で, それらが基本的データでどう表されているかにももっと無関係である. プログラム言語の合成オブジェクトを直接扱う機能の重要性を, この例は示している: これなしにはlinear-combinationのような手続きが, 引数を, その内部構造を知ることなく, addやmul に渡すことは出来ない.1
本章は上述の有理数算術演算システムの実装で始めよう. これは合成データやデータ抽象を議論する背景となる. 合成手続きと同様, 問うべき主題は複雑さに対処する技法としての抽象の問題であり, データ抽象により, プログラムの部分部分で適切な 抽象の壁(abstraction barrier)を建てることが可能なことを見ていこう.
合成データを形成する鍵は, データオブジェクトを組み合せて更に複雑なデータオブジェクトを形成するため, プログラム言語に備っている, ある種の「糊」である. 糊もいろいろあり得る. 特別な「データ」操作は全く使わず, 手続きだけで合成データを形成する方法を知るであろう. 「手続き」と「データ」の区別は, 1章の終りの方ですでに希薄になっていたが, これでますますぼんやりしてくる. 並びや木を表現する通常の技法も研究しよう. 合成データを扱う重要な考えは 閉包(closure) ---データオブジェクトを組み合せるための糊は, 基本的データオブジェクトだけでなく, 合成データをも組み合せる---の考えである. もう一つの重要な考えは, プログラムを取り替え引き替えして組み合せる時, 合成データオブジェクトは 公認インターフェース(conventional interfaces)として役立つということである. これらの考えを, 閉包を活用する単純な図形言語を使って示す.
次に言語の表現力を, 記号式(symbolic expressions)---その要素部分が単に数だけでなく, 任意の記号であるデータ---を使って論じよう. オブジェクトの集合を表現する, いろいろな代案を検討する. 数値関数がさまざまな計算プロセスによって計算されるのと同様に, データ構造をより単純なオブジェクトを使って表現するにも, 多くの方法があり, 表現の選択がデータを扱うプロセスの必要とする時間とスペースに大きく影響することを見る. これらの考えを記号微分, 集合の表現, 情報の符号化などの例で検討しよう.
続いてプログラムの異った部分では異って表現されているデータを扱う問題を取り上げる. これは多くのデータ型を扱う
汎用演算(generic operations)の実装を必要とする. 汎用演算の下で部品化力を維持するには, 単純なデータ抽象で建てることが出来るのより, 更に強力な抽象の壁が必要である. 特にデータ主導プログラミング
(data-directed programming)を導入し, その技法により, 個々のデータ表現を独立に設計し, それから
加法的に (additively)(つまり修正なしに)組み合せられるようにする.
システム設計におけるこの解法の能力を示すため, 本章の終りでは, 多項式上での記号演算を行うパッケージの実装に, 学んできたことを応用しよう. この多項式では, 係数は整数, 有理数, 複素数のほか, 他の多項式であったりもする.
1
手続きを直接扱う機能もプログラム言語の表現力を似たように増やす. 例えば1.3.1節で, sum手続きを紹介した. それは手続きtermを引数にとり, 指定された区間にわたってtermの値の総和を計算した. sumを定義するのに, termのような手続きを一人前のものとして扱い, termがより基本的な演算でどう表されているかを見ないですむことが大切である. 仮に「手続き」という考え方がなければ,
sumのような演算を定義することが出来ると考えたかどうか疑わしい. また総和の実行に関していえば, termがより基本的操作からどう構成されているかにも無関係である.