lethevert is a programmer このページをアンテナに追加 RSSフィード

29/3/2006 (Wed) 晴れ

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

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の文法に慣れてきて、なにかまとまったプログラムを書こうとしたときに、絶望的な壁にぶつかったような気がすることがあると思いますが、それの正体がきっとこれです

sumiisumii 2006/04/01 09:16 この例だけだと、どう見ても状態を使った設計より、状態を使わない(純粋関数型)設計のほうがシンプルなので、むしろ「設計からして『状態』はできるだけ使わないほうが良い」という逆の結論になってしまうような気が…

sumiisumii 2006/04/01 09:30 補足ですが、もし「オブジェクトには(破壊的代入の対象となる)状態がある」と「定義」するのであれば、「純粋関数型言語には状態がない」ので、オブジェクト指向と純粋関数型プログラミングが相反するのは(定義より)当然です。

もしそういう定義なのであれば、状態を使った設計と使わない設計とどちらか良い(ことがより多い)か、という話なのだと思います。もちろん、現在の世の中では状態を使った設計のほうが多いわけですが、本当はどうなのか、という…

lethevertlethevert 2006/04/01 13:53 そもそも「状態」を持たないプログラミングなんてないので、「状態を使った設計と使わない設計とどちらか良い」ではなくて「状態をどう表現するか」というじゃないかと思います。あと、「逆の結論」といっても、どっちがいいとかいう話もここではしていないです。そもそも、私はClean Loveな人ですから。まあ、それは本題ではないですが。

本題の方は、今日の記事に書きます。

lethevertlethevert 2006/04/01 15:03 と思ったけど、やめます。
オブジェクト指向という言葉がそもそもオーバーロードされすぎていて、厳密な議論をするのに向かなくて、書けば書くほど書きたいことから離れていく気がするので。たとえば、CLOSというバリエーションまで考え始めると、何がなにやらわからなくなってしまう。

ただ、オブジェクト指向言語の代表例としてJavaを取り上げると、コンストラクタで重たい処理を書いても平気だったり、フィールドを隠蔽して外部からのアクセスを制限したり、オブジェクトを生成するときにキーワードを要求したりすることを考えても、代入を基本的な機能として受け入れていると考えることは自然なんじゃないかと思います。

あと、現状のオブジェクト指向言語は、代入をあたりまえのように使っているわけで、それに対して、理論的に代入を使わないプログラミングができるからという理由で、オブジェクト指向と代入には関係がないと言い切るのはどうかと・・・
まるで、現状のC言語がポインタ演算をあたりまえのように使っているのに対して、理論的にポインタ演算を使わないプログラミングができるからという理由で、C言語とポインタ演算には関係がないと言い切っているような違和感を感じます。

sumiisumii 2006/04/01 18:26 主な点はsucceedさんのエントリのコメント(http://d.hatena.ne.jp/succeed/20060331#1143826028)に書いたので、そちらにまかせるとして、JavaやC++が「オブジェクト指向言語の代表」であるというのは、それまでの長大な歴史や研究を無視した、あまりにも近視眼的な(最近の流行しか眼中にない)見方ではないかと。世間でもそうですが、それがどうも「オブジェクト指向に破壊的代入は必須」というmythにつながっているように思います。(逆に言うと私の考え方などは最近の流行とは大きく異なっているわけですが)

lethevertlethevert 2006/04/01 19:12 「JavaやC++『のみ』がオブジェクト指向言語の代表」というのは間違っていると思いますが、「JavaやC++がオブジェクト指向言語の代表」というのは間違っていないのではないでしょうか?それとも、JavaやC++って、オブジェクト指向の傍流ですか?それだとすると、認識を改めなければならないとは思いますが。

lethevertlethevert 2006/04/01 19:25 ところで、代入のないオブジェクト指向言語の代表ってなんでしょう? O’Haskell?

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

 
Connection: close