純粋関数型とオブジェクト指向

[id:lethevert:20060328:p3]の続き。
純粋関数型とオブジェクト指向がどのようなプログラミングの違いをもたらすかについて、簡単な例を挙げて考えてみます。

例題

工場(Factory)のプログラムで表現します。
工場は、資材(input)を受け入れ(accept)た後、「開梱(unpack)」「組立(assemble)」「包装(pack)」の3つの段階を経て、完成品を生産(produce)し、出荷(ship)されます。

オブジェクト指向

その問題に登場する「人やモノ」に注目して、プログラムのデザインをします。ここでは「工場」と、そのラインを構成する「開梱工程」「組立工程」「包装工程」に注目して、プログラムを構成します。

class Factory {
  var acceptant; // 最初の工程。資材の受け入れ。
  var backyard;  // 最後の工程の出力を蓄える一時保管場所。
  
  // コンストラクタで、工場のラインを組み立てる
  // 各工程は独立したオブジェクトで、
  // 後からラインを組替えられるように、工程間はゆるい結合になっている
  constructor Factory {
    // まず、工程オブジェクトの生成
    backyard = new Backyard();
    var unpacker = new Unpacker();
    var assembler = new Assembler();
    var packer = new Packer();
    // 続いて、工程の順序関係を定義
    acceptant = unpacker;
    unpacker.setNext(assembler);
    assembler.setNext(packer);
    packer.setNext(backyard);
  }
  
  // 工場が資材を受け入れて、製品を生産します。
  // 各工程は、accept()メソッドを持ち、入力を受け取ります。
  // accept()メソッド内では、各工程の作業を行って、次工程のaccept()メソッドを呼び出します。
  // accept()メソッドの返値はありません。なぜなら、各工程は、最終生成物を知らないからです。
  // 完成品は、backyardから取り出します。
  function produce(input) {
    acceptant.accept(input);
    var output = backyard.ship();
    return output;
  }
}

以上、非常に単純ながら、オブジェクト指向的な分析と設計によるプログラミングの一例です。
このプログラムで、代入がどのような役割を果たしているかを考えてみてください。そして、代入がない場合に、このプログラムが成立するかを考えてみてください。
Factory#produce()の中で、accept()メソッドとship()メソッドを見れば、これが代入なしには実装し得ないことがわかると思います。accept()メソッドがoutputを返すようにすればよいように考えるかもしれませんが、もしそうすれば、ここで使われているUnpackerクラスや他の工程クラスは、他の製品の生産には再利用できないことになってしまいます。accept()が返値を持たないことが、オブジェクトのモジュール性を高めて、再利用可能にしている肝の仕掛けになっていて、それは、代入が暗黙の前提になっているのです。

純粋関数型

純粋関数型では、言語から代入を排除しているため、上のようなオブジェクト指向的な分析と設計によるプログラミングは全く役に立ちません。では、どうすればよいのでしょうか?
(この続きは、夜にまた書きます。興味のある方は、それまで考えてみてください。)
純粋関数型が注目するのは、「流れ」です。オブジェクト指向的な分析で描いた設計図に、時間軸を足してみてください。そしてその中にある流れに着目してみてください。
この問題では、「資材が開梱され、組み立てられ、包装されて完成品が作られる」という流れがあり、それが「生産」を構成しています。
Cleanで表現してみます。

produce input = pack (assemble (unpack input))

ここで、unpack, assemble, packは全て関数です。関数は、入力を1つとり、適切に加工して、出力します。そして、そのような関数をつなぐことで、大きな関数を作ります。
上の表現は次の2つのバリエーションがあります。まず、生産という流れが開梱と組立と包装という流れの組み合わせからできていることに注目した書き方がこれ。

produce = pack o assemble o unpack

そして、そこに流れがあることを強調した書き方がこれ。

produce input = input --> unpack
                      --> assemble
                      --> pack

(この2つ目の表現は、Haskellモナドと基本的に同じです)
ここで留意しておくべきは、状態は関数の入出力としてのみ表現され、何らかの変数から取り出すことはできないということです。それは、代入がないことからの必然的な帰結です。

考察

オブジェクト指向は、分析や設計のためのさまざまな提案がなされていて、それらの力を援用することで個別の問題を分析し設計することができますが、そのような設計は代入があることを暗黙に仮定しています。
特に、オブジェクトをシェアすることで、オブジェクト間の情報の受け渡しを行うという手法は、頻繁に使われますが、この基本的なアイデアが代入によって成り立っています。たとえば、上の例では、backyardオブジェクトが、factoryオブジェクトとpackerオブジェクトでシェアされ、完成品の受け渡しに使われています。
しかし、純粋関数型では代入がないので、オブジェクトをシェアして情報の受け渡すことができません。このことは、純粋関数型でオブジェクト指向プログラミングを行うことを、実質的に不可能にします。オブジェクト指向的な分析や設計もそこでは役に立たず、根本的に発想を転換することを強要されます。(上の例では、上手くマッピングできそうな気がしますが、あれは問題が単純な上に、説明のためにわざと対応付けを行ったからで、現実にはそんなに上手く対応付いてくれるわけではあありません)*1
ということで、純粋関数型とオブジェクト指向の間には大きな壁が立ちはだかっているのですが、この壁を乗り越えるアイデアはあるのか? そもそも、乗り越える必要があるのか? というところは、私はまだよくわかっていないのです。今は、上の例で少し触れたように、オブジェクト指向的分析・設計に時間軸を添えることで、分析・設計段階で純粋関数型とオブジェクト指向の関係を見出すことができるかも知れないというアイデアがあるだけです。

*1:この制限は、強烈です。最近、本も出版されて、Haskellを勉強する人が増えてきたみたいですが、Haskellの文法に慣れてきて、なにかまとまったプログラムを書こうとしたときに、絶望的な壁にぶつかったような気がすることがあると思いますが、それの正体がきっとこれです