関心の局所化と静的な記述

プログラミング言語の基礎知識」のメモ

(イントロ部分は省略)
原理的には、プログラミング言語には、チューリングマシンと同等の計算記述能力(+外部リソースを扱う機能)があれば良い。しかし、複雑なプログラムを書くのは簡単な作業ではない。
ではプログラミング言語にどんな機能があるとうれしいか。
→「コンピュータにさせたいこと=複雑な処理」だけど複雑な処理をそのまま記述するのは大変。だから、複雑なことをなるべく複雑でないように記述できる機能があればうれしい。
複雑なことを単純化にするには分割していく。「困難は分割せよ」。
プログラムを小さな部品に分割する=プログラムを小さな部品から組み上げる。

プログラムを分割するための手引き: 関心の局所化と静的な記述

小さな部品に分割したときに、その部品やその周辺の小さな部分だけを見て容易に理解できるのが望ましい。一方、実際の時系列に沿った動的な変化・処理の流れを追うと、プログラムの広い範囲の理解が必要になる。また動的な流れ・変化よりも静的な関係・構造の方が人間にとって理解が容易。
だから「処理の流れ・状態の変更」を直接記述することから離れると、局所的な理解が容易になり機能の分割・合成もおこないやすくなる。

  • 処理や状態の動的な流れ・変化を直接記述する機能よりも、高レベルの制御構文。
    • gotoよりもブロック型の制御構造。
    • 繰り返し処理で、ループカウンタなど状態を表す変数を直接操作しない(内部イテレータ)。
  • 動的な処理・制御を(抽象化や自動化によって)隠したり分離したりする。
    • メモリ管理(オブジェクトの動的確保・破棄)の自動化。
    • 動的結合・メソッド結合・アスペクトの織り込み・バックトラックなど、制御の流れがプログラムテキスト上に直接現れない言語機能。
    • 遅延評価のように、実際の処理の流れとプログラム記述とを分離する言語機能。
  • 「(動的な)処理の流れ、状態の変更」から「(静的な)関係、値」へ。関数型プログラミング
  • 「処理が主、データが従」から「データが主、処理が従」へ。オブジェクト指向プログラミング。

→ プログラムの記述をより静的なものにする(そのために言語はより動的な機能を持つ)。命令的記述から非命令的(宣言的)記述へ。
竹内郁雄「「宣言型」って何?」(『bit』2000年9月号)より引用:

筆者がここで主張したい「宣言型」とは、人間にとって不得手な時系列の取扱いを少しでも楽にしようという方策全般を指す。

時系列を縮約させて、見える空間上へ射影するとは、別の言い方をすれば、動的なものをできるだけ静的にとらえようとする方策である。これはいみじくもダイクストラの有名なgoto有害論の論旨でもあった。

プログラムの動きの断面をとらえるという考え方も新しいものではない。例えば、フロイドの表明法はその最初のものだろう。プログラムの要所要所で、成り立っているべき論理式の集合を考えたり、ループの中での不変式を考えたりすることは、制御構造の抽象化だけではできないデータの動的特性の把握を容易にした。

筆者がSmalltalkを見て、オブジェクト指向が現実のプログラミングに役立つと直観したのは、ここに述べてきたような理由からである。理想は「個々のオブジェクトを記述しなさい。そうしたらみんなちゃんと動きます」である。ここでオブジェクトを記述するというのは、断面に見える、ダイナミクスを内包したオブジェクトである。

並列・並行プログラミングでは、ただでさえわかりにくい時系列が絡み合う。だからますます時系列の捨象が本質的になる。

関数型言語

関数の組み合わせの形でプログラムを記述する。
ここでいう関数というのは、参照透明性を持つ(同じ入力に対して同じ結果を返して、外部に対する副作用・状態変更を引き起こさない)計算手続きのこと。ただし現実的には、入出力その他で状態変更・副作用が必要になるので、多くの関数型言語では参照透明性を破る関数・機能を持っている。またSchemeのように、関数型言語の特徴を多くそなえた手続き型言語も、関数型に分類されることがある。
関数型言語の特徴・機能。

  • 変数の値、オブジェクトの状態を変更しない: 個々のオブジェクトを不変に保つために、必要に応じて次々新しくオブジェクトを生成することになる。そのためオブジェクトの動的な生成・破棄を容易にする機能があると良い(GCLispのcons)。オブジェクトの不変性と相性が良いデータ構造(特にリスト)のサポート。
    利点: オブジェクトを共有していても、共有している他の部分に影響を与えない。処理・動作の順序に依存しない。→ 関数(処理)の影響が局所化される。
  • 静的スコープの関数オブジェクト(クロージャ): 処理自体を、プログラム内で組み合わせたり持ち運んだりが可能な部品として扱う。関数の挙動(引数の値以外)が関数の定義された位置で静的に決まるので、動作が局所的・静的に把握しやすくなり、プログラムを組み立てる部品として使いやすい。
    (クロージャという用語は、自由変数の参照が定義環境に閉じている(=呼び出し環境の影響を受けない)ことに由来する。DSpace@MIT: The Function of FUNCTION in LISP, or Why the FUNARG Problem Should be Called the Environment Problem p.12、DSpace@MIT: Lambda: The Ultimate Imperative p.2)

→ 挙動や影響を局所的・静的に把握できるようになるので、データや関数を部品として扱いやすくなる。

オブジェクト指向言語

オブジェクト指向言語一般は歴史的にややこしくて範囲が広すぎるので、特に動的結合に注目する。

  • 動的結合……データの種類に応じて、実行する手続き(メソッド、関数)を動的に選択する機能。

手続き型言語が持っている標準的な手続き機能(サブルーチン、関数)に基づいてプログラムを分割する場合、処理中心の分割になりがち(手続きや関数というのは処理や機能をまとめるためのものなので)。
例えば、何らかの一連の処理をおこないたいとして、その処理をおこなう対象データには複数の種類があるとする。標準的な手続き機能を自然に用いた場合、処理の種類に基づいたプログラム分解がおこなわれるだろう。たとえばこんな風に。

処理1(データx) {
  case データx             ; xがどの種類のデータかによって処理を選択
  種類A => 処理1A(データx)
  種類B => 処理1B(データx)
  ……
}
処理2(データx) {
  case データx
  種類A => 処理2A(データx)
  種類B => 処理2B(データx)
  ……
}
処理3(データx)
……

ここでは処理の種類(処理1、2、3)がプログラム分解の主軸で、データの種類(種類A、B)による分解は下位の構造になっている。
しかし動的結合機能を使うと、例えば次のようにデータの種類を軸にしたプログラム分解がおこなえる。

種類Aのデータについての処理 {
  処理1(データx) {
    処理1A(データx)
  }
  処理2(データx) {
    処理2A(データx)
  }
  処理3(データx)
  ……
}

種類Bのデータについての処理 {
  ……
}
種類Cのデータについての処理 {
  ……
}

このようなデータ中心の分解の方が常に良いとは言えないけれど、動的結合を使うと、

  • 処理中心の分割とは異なる分割手法が手に入る。関数型言語的な関数分割との併用も可能(例: Haskellの型クラス)。
  • 具体的な処理の選択が実行時まで遅延され動的に選択されるので、(具体的な内部構造・処理を外部から隠せば)部品としての独立性を高めることができる。
  • 処理を選択するための制御構造がプログラム上から消えるので、プログラムの記述はより静的になる。

という効用がある。
(※ この書き方だと「関数型言語はあまりデータ中心ではない」という誤解を与えそうなので少し補足。静的型付けの関数型言語では「まず適切な型を定義し、それに対してプログラムする」という考え方がしばしばなされる。またLispSchemeでは多くの場合、扱おうとする問題や対象をリストで表現するところからプログラミングが始まる。リストによって表現されたデータが関心の中心部にあり、その周りにさまざまな処理が書かれていく。これらはどちらもデータ中心的な視点でおこなわれている。データ構造の記述とアルゴリズムの記述を比べると、データ構造の記述の方が静的なので、静的な記述を求めていくとデータ中心という視点が多かれ少なかれ入ってくる)

動的結合に限らず、処理を動的に選択したり合成したりする機能(メソッド結合やアスペクトの織り込みなど)があると、処理の流れや制御構造がプログラムの字面の上では消えた形になり、より静的な記述になる。また実際の処理の流れから特定の部分だけを分離できるので、記述をより局所化することが可能になる(ただし、これらの機能を使いつつ動的な処理の流れに依存するようなプログラムを書くと、goto以上に分かりにくいジャンプ機構にもなりえる)。オブジェクト指向からは離れるけれど、遅延評価も、処理順序を動的に自動で決定するという点では、これらと類似の機能と見ることができる。

(動的結合と機能分割についての別の説明: オブジェクト指向の一側面: メッセージ送信と動的結合)