この記事の補足を書きました。併せてご覧いただけると幸いです。
[「関数型プログラミングはオブジェクト指向の正当な後継である」がわからない理由]
(http://qiita.com/retemo/items/ac2f35687c82192e8f30#_reference-615873c29073dacce79f)
対象読者
この記事はオブジェクト指向設計を本格的に学びドメイン駆動設計や責務駆動設計等を実践したことがある人々に「オブジェクト指向と関数型プログラミングの関係」を深く知ってもらうことを目的としています。これらの人々の中には手に馴染んだオブジェクト指向に未だに固執している人や、関数型プログラミングが気になってSwiftやScalaを触り始めているがイマイチ関数型プログラミングの本質が見えていない人も多いと思います。そうした人々が次の一歩を踏み出すキッカケになれば幸いです。
なぜこの記事を書こうと思ったのか?
IT系の情報サイト等で「Haskellがすごい」という記事を見かけるようになってからもう10年近く経とうとしています。私自身もこれまでに何度か関数型プログラミング言語をかじってきましたが、実務としてはその間のほとんどをオブジェクト指向とともに過ごしてきましたし、その状況はSwiftが登場した後もしばらく変わりませんでした。とはいえ最近は関数型プラグラミングのことを知らないと肩身を狭く感じる機会が多くなりSwiftで関数型プログラミングを本格的にはじめたのですが、残念なことに、今まで「オブジェクト指向と関数型プログラミングの関係」についての納得できる記事を目にしたことがありません。そこそこ信用できそうな記事を見かけても、どれも実際の使用感との微妙なズレがあったのですが、そんな中、あるブログで「関数型プログラミングはオブジェクト指向の後継にあたると考えている」という記述があるのを見て共感を覚えました。残念ながらその記事からも具体的な理由は読みとれなかったのですが、であれば「そのテーマで自分なりの考えをまとめよう」と思い立ったのがこの記事を書こうと思ったきっかけです。
いささか刺激的なタイトルですが、最後まで読んでいただければあながちデタラメではないことを理解してもらえると思います。
なお、この記事はそれなりに研鑽を積んだ人を対象にしていることもあり「オブジェクト指向」や「関数型プログラミング」、各種設計手法などの解説は行いません。あくまで両者の比較に必要な部分だけしか扱わないので必要なら適宜他の資料を確認してください。
さて、心の準備はよろしいですか?
人によっては無茶な要求を突きつけられることになるかもしれませんがあしからず。
なぜ関数型プログラミングはわかりにくいのか?
まず最初にこの記事の基本スタンスを明らかにしておきましょう。
関数型プログラミングの特徴は良くも悪くも「数学的根拠に根ざしている」ことにあります。よく考え抜かれた便利な仕組みを持っている反面、それを数学の言葉で解説されても一介のプログラマーには理解できません。関数だけをとっても理解しづらいのですが、さらに高度な概念で構成されるモナドについては聞けば聞くほど混乱が増すばかりです。
この問題をなんとか改善しようと言葉を選んでいるケースもあるのですが、それはそれで「詩的」というか「俳句的」というか、”ふんわり"し過ぎな傾向があります。
言い換えると「厳密だけど素人には扱えない物差し」と「比喩的な物差し」しか使われてこなかったことが関数プログラミングが分かりにくい原因だと私は考えています。ですので、ここでは「定性的な物差し」をオブジェクト指向の中から切り出しながら解説してみたいと思います。
オブジェクト指向の負の遺産を捨てよう
ではオブジェクト指向から比較材料を切り出してみましょう。
いきなりですが皆さんはオブジェクトってなんだか説明できますか?
“Object"を和英辞書で引くと大抵最初に「物体」という訳語が出てきます。そのせいか「オブジェクト=物」という観点で書かれた書籍や文章が大量に存在します。有名どころでは「実践UML」などが挙げられますが、まずこの考えを捨てましょう。「物・指向」もオカルト的で面白いのですが少なくともこの記事では扱いません。辞書にはさらに「対象、目的(語)、客体」という意味も載っていますがこれらもちょっと違います。さらに詳しい辞書には「個体」という訳語が載っていますが(つまり、あまり一般的な意味ではありませんが)これがオブジェクト指向のオブジェクトです。経験豊かなエンジニアなら「オブジェクト=カプセル化」が正しいことを経験的に知っていると思いますが「オブジェクト=個体=カプセル化」とすればさらに合点が行くのではないでしょうか? 漢字文化圏では「子(し)」が近い概念で、独立した個としての存在を表しています。ためしに「データ・オブジェクト」を言い換えてみましょう。「データ物」というより「データ子」という方がしっくりくると思います。
ではオブジェクト指向のオブジェクトは「何から何が独立している」のでしょうか?
答えは「コンピュータからコンピュータが独立している」です。言い換えると「入れ子状のコンピュータ」です。ここでいうコンピュータとは「現実」のコンピュータのことではなく、古き良きチューリングマシンから今日の仮想化技術までを含む「抽象的な概念」としてのコンピュータです。
”Object oriented”という言葉を生み出したアラン・ケイは当初からオブジェクトのことを仮想機械と考えていて、後にオブジェクトのことを“real computers all the way down (RCATWD)”「 徹頭徹尾本当のコンピュータ」と表現しています。この考えに従えばオブジェクトは後の「アクター指向」や「エージェント指向」も内包していたはずですが、実際にはC++やJavaに代表される「継承と多態性」を中心としたオブジェクト指向の方が広まってたせいで、仮想機械としてのオブジェクトは広く知られてきませんでした。
人ではなく技術そのものに注目しよう
偉大な導師、アラン・ケイのネーミング・センスの無さには私も一言いたいのですが、ここではこうした人物評も控えることにしましょう。
「アラン・ケイのオブジェクト指向 VS ストロヴストルップ(C++)のオブジェクト指向」という対立の構図は、純粋に技術的な視点に立てば「独立したオブジェクト間をメッセージングでつなぐ技術」と「類似オブジェクトの関係を応用して制御を隠蔽する技術」という個別の技術の話題となり、そこには優劣はありません。これらはいずれもソフトウェアを設計する上で考慮しなければいけない事柄ですから人間社会のポジション争いの事は一端忘れて、技術的な視点に徹しましょう。
(政治抗争的な図式が人の興味の対象であることは認めますが、それで技術の本質が見えにくくなるようでは本末転倒です。)
先の2つはカプセル化の特性をプログラミング言語に活かした例ですが、それと同様にカプセル化の特性を設計に活かした代表例がドメイン駆動設計や責務駆動設計です。もしこれらのことをあまり知らず、それらの原典に当たりたいなら「エリック・エバンズ ドメイン駆動」や「ワーフスブラック 責務駆動」等のキーワードで検索してみてください。ただ、これらについてもこの記事では人ではなく技術そのものに注目しますから、ドメイン駆動は「問題領域の区割りと仮想機械の区割りを関連付ける技術」、責務駆動は「仮想機械間の役割分担」と見なすこととします。これら4つがオブジェクト指向を代表する技術トピックでしょう。(大きなトピックで見落としがあるようならコメントにお願いします。)
この4大トピックが1つ目の「定性的な物差し」です。
抽象的コンピュータの構成要素
「仮想機械」や「ワーフスブラック」の名前が上がったところで抽象的コンピュータの構成要素についても触れておきましょう。仮想機械というとステートマシン(オートマトン)が有名ですね。そのステートマシンは「状態」と「遷移」、そして遷移を引き起こす「イベント」で構成されます。イベントにはトリガーやガード条件を含まれていて、「制御」と言いなおすことができます。同様に状態と遷移も「変数」及び「演算」と言い換えることができます。この「変数、演算、制御」がプログラミング言語や抽象的コンピュータの3大要素です。
オブジェクト指向の黎明期に責務駆動設計を提唱したレベッカ・ワーフスブラックはこれらにさらに要素を加えて「保持役(状態役)、サービス役(演算役)、制御役、インターフェース役、構造役、調整役」の6つをオブジェクト(仮想機械)の典型的な役割(ロールのステレオタイプ)としました。これらの3大要素や6大ロールはモナドを俯瞰するのに使いますから覚えておいてください。
これが2つ目の物差しです。
関数型プログラミングの概要
さて次は関数型プログラミングの番です。今回、オブジェクト指向との比較材料として扱うのは「モナド」と「型」なのですが、代表格である関数自身もわかりにくいので、まずは関数の大枠の解説から始めましょう。
関数型の関数は何が違うのか?
関数型プログラミングの関数の特徴を簡潔に理解しようとすると、注目すべきは「参照透過性、関数合成、部分適用」の3つです。これらを順に見ていきましょう。
歴史的に見ると関数型の代表的な技術である「ラムダ式」などは2000年頃から他の言語でも実用化されているため(C++のBoostライブラリなど)、技術的にはあまり目新しさを感じませんが、その設計思想には多くの普遍的要素が含まれており、近年改めて注目を集めるようになっています。その代表格と言えるのが「参照透過性」です。
私も今日では(JavaやObjective-Cで実装するときでさえ)この「入力が同じなら必ず同じ答えを返す」という「参照透過性」を抜きに設計するなど正直考えられません。
開発をやっていて一番辛いことの一つが「頭の中で考えた設計に何らかの問題があり、しかもそれが実際に動かしてみて初めてわかる」というケースです。「参照透過性」を持たせると関数ごとの動作が確定して考察しやすくなるため、こうしたケースをかなり減らすことができます。
(PascalとかC言語全盛の頃に「データ構造の設計が良い方が関数(もちろんCの関数)の設計が良いより、ずっと良い」みたいなことが言われていましたが、関数型言語ではこれは逆転するかもしれませんね。)
次に「関数合成」ですが、前述のようにこれは関数型のおハコではありません。が、逆に言えば今さら説明する必要がないってことですよね?
それから関数型プログラミング特有の機能なので「部分適用」を知らない人も多いと思いますが、名称から「関数合成」と関係してそうなことは想像ができると思います。「部分適用」というのは「複数の引数を受け取る関数」に対して部分的に引数を渡すと、関数からは「足りない分の引数を取る関数」が返ってくる機能(というか技術)です。つまりたくさんの引数を持つ関数を段階的に実行することができます。
参照透過性を実現しようとすると引数の数が多くなりがちで、オブジェクト指向言語では引数のデフォルト値を使ってこの問題に対処しますが、関数型プログラミングではこの「部分適用」を用います。
これらの「参照透過性、関数合成、部分適用」の3つのおかげで関数型プログラミングの関数は関数の堅牢さと柔軟性を両立させることができると言えるでしょう。
他にも関数型言語の関数には「変数として扱える第一等関数」とか「値を変えられない変数」とか「カリー化」など様々な特徴がありますが、これらは「参照透過性、関数合成、部分適用」の3つを実現するために必要な「縁の下の力持ち」として捉えるとすっきりすると思います。
追記
そういえばクロージャも関数型由来なんでしたっけ?今更なんで忘れてました。
モナドのない関数型なんて
(............クリープを入れないコーヒーなんて......... by 森永乳業)
....さて本題に戻りましょう。
今日の関数型プログラミングにおいて一番便利なのはやはり「モナド」です。オブジェクト指向では耳にしない言葉ですから意味を知りたがるのが人情でしょうが、ここでは構造的な概略にだけ触れておきます。
(誤解を減らすために一応書いてますが、いきなりこれを読んでもわかりにくいので読み飛ばしても構いません。その場合は後で読み返してみてください。)
- モナドは「関数適用用のメソッド」を持つ「ジェネリック・オブジェクト」として実現されます。
- 関数適用に用いるメソッドは「関数」を引数として取り、実行結果を「新たなモナド」として返します。
- この「モナドが返る」というのがポイントです。メソッド・チェーンの条件と似てるからあとは想像つきますよね?
- 他にもラップ/アンラップなどがモナドには必要です。
- これらとモナド則さえ守っていれば後は「各々目的ごとに自由にやってください」というのがモナドです。
- このため各言語には目的に沿った様々なモナドが準備されています。
(我ながらアバウトな説明だなぁと思いますが、実際こんな感じでも使えるんですよねぇ。)
もっと詳しくモナドについて知りたいのであれば「モナドとは何か」を説明しようとする試みはインターネットのあちこちで目にすることができますし、特にモナド則などについての数学的な説明であれば簡易なものから厳密なものまで一通り解説が揃っています。数学的な概念を展開しているヤツは難解ですが、実用例とモナド則に絞った解説なら頑張れば大抵の技術者が理解できると思います。
一方で経験則からモナドとは何かを説明しようという試みはほとんどが失敗しているようです。理由はモナドの応用範囲があまりに広いので経験則でその全貌を掴むのが難しいためです。どうやらフリースタイルでは太刀打ちできない様ですから、モナドの使われ方を理解するために対比できる物差しを準備しましょう。ここで先ほどの「仮想機械の構成要素」の登場です。
モナドとオブジェクトの典型6役「保持役、サービス役、制御役、インターフェース役、構造役、調整役」を比較するとモナドの応用範囲の広さがわかります。なぜってモナドは6つのロールの全てになることができるからです。
例えばHaskellのIOモナドはインターフェース役であることが一目瞭然ですね。ArrayモナドやDictionaryモナドあるいはリスト・モナドが構造役であることも疑う余地はないでしょう。また、モナドは「関数と対を成す存在」としてデザインさせれていますから、関数以外の全てに対応してそうだと予想することもできますが、実は有名な「すごいH本」にモナドを使って関数合成が行えることが記述されています。つまりモナドは合成関数を作ってサービス役にもなれるわけです。
これじゃ「何でもあり」のオブジェクトと同じですよね。なんか既視感を覚えてしまいます。
でも大丈夫、つかみどころの無いモナドですが、実際には保持役や関数合成には他の仕組みが用意されていますから、これらにモナドを使うことはまず無いでしょう。であれば自ずとモナドの使い道は狭まってきます。仮想機械の3大要素で残っているのは「制御」のみです。
結局のところ、モナドの主な使い方は「保持役(状態役)に暗黙の制御構造を付与すること」になります。例えばSwiftやJava8のOptionalモナド(HaskellではMaybeモナド)はジェネリック(Haskellでは型コンストラクタ)ですので値や状態を直接保持することはできません。nil(null、HaskellではNothing)という新たな状態を持てるようになりますが単体ではあまり意味をなさず(というか使いづらい)、それが本領を発揮するのは関数をOptionalチェーンで繋げる時です。Optionalチェーンではどこかで失敗すれば(nilが返れば)そこから先の処理は呼ばれませんから明らかに条件分岐を隠し持っています。
関数型プログラミングの先駆者たちはモナドを表現するのに「器」と「文脈」という表現をよく用います。例えばOptional(maybe)モナドのことを「失敗するかもしれないという文脈を状態に付与するための器」と表現します。「器」というのは極論すればジェネリック・オブジェクトのことで、これは言い換えれば「失敗した場合の暗黙の条件分岐を含んでいるジェネリック・オブジェクト」となります。モナドは連結できるので複数の隠れた条件分岐が連なり、それまで含めて文脈と表現しているのだと思われます。
この「暗黙の制御構造」はとにかく便利です。何しろモナドを指定すれば一々、制御句を書かなくて済むのですからコード量も少なくなりますし、モナドが内包する関数適用(mapメソッド等)を使えば、モナドと関数の記述順をひっくり返すこともできますから、一度これらを経験するとモナドが使えないケースで制御句を書くのですら面倒になります。(笑)
「阿吽の呼吸」とも言うべき使いやすさの拡張
ところでこの「暗黙の制御構造」ですが、同様なものをオブジェクト指向で目にしたことはありませんか?
そう、「ポリモーフィズム(多態性)」です。
先に挙げたオブジェクト指向の4大技術トピックのうち「類似オブジェクトの関係を応用して制御を隠蔽する技術」言い換えれば「継承と多態性の技術」は、1つのメソッド名で型に応じた別個のメソッドを自動的に呼び分けることができます。つまりは型をパラメータとしたswitch文的な条件分岐が暗に含まれているわけです。その効果についてはオブジェクト指向に馴染んだみなさんには説明するまでもありませんよね?
分かりきった手続きについての言及を省略した、言い換えれば「阿吽の呼吸」とも言うべき、そのユーザーフレンドリーな「暗黙の制御構造」の応用範囲をモナドは広げてくれるわけです。実際、多態性とモナドの使用感はよく似ています。ですので実際に使ってみれば少なくともモナドについてはオブジェクト指向の後継であると実感できると思います。
型にまつわる考察
「継承と多態性の技術」の話が出たところで、それと関連する「型」についてもオブジェクト指向と関数型プログラミングを比較してみましょう。
抽象的な記述しかしないのでちょっとわかりにくいと思いますが自分なりに咀嚼しながら頑張って付いてきてください。
「型(Haskellでは型インスタンスとも言う)」というのはごく簡単に言うと「構造」と「扱い(振る舞い)」を「シンボル(名称)」で繋いだものです。「構造を抽象化した型」は「ジェネリック(C++ではテンプレート、Haskellでは型コンストラクタ)」で、「扱いを抽象化した型」は「インターフェース(Swiftではプロトコル、Haskellでは型クラス)」であり、オブジェクト指向と関数型プログラミングのいずれにも存在しています。「シンボル」がさらに抽象化されることはありませんが「別名(Typedefや型シノニム)」があり、これも双方に存在します。と、ここまではオブジェクト指向と関数型プログラミングの間に大きな違いはありません。
それから話が「継承と多態性」に戻りますが、純粋な関数型プログラミング言語であるHaskellもオブジェクト指向のそれとほぼ同等の多態性を有しています。Haskellでは型同士の継承をサポートしていませんが、型クラスから型インスタンスへの継承はできるため、それを利用した多態性の実現が可能です。(Swiftの発表以降Appleも「プロトコル指向」を推進していますから、今後Haskell型の多態性が主流になる可能性もあります。)
型関連での両者の大きな違いは、オブジェクト指向では「型=オブジェクト=カプセル化」あるのに対し、Haskellでは型とカプセル化が結びついていないことです。とはいえパッケージを用いてカプセル化自体はできます。ついでに言うと「型とカプセル化」が分かれていることはオブジェクト指向の亜流である「アスペクト指向」の思想にも通じます。
オブジェクト指向の技術トピックは関数型プログラミングでも適用できる
さて、お気づきでしょうか?
これで先述のオブジェクト指向の4大技術トピックのすべてが関数型プログラミングにも適用できることがわかりました。
「継承と多態性の技術」=「類似オブジェクトの関係を応用して制御を隠蔽する技術」については先に述べた通りですが、それに加えて、カプセル化ができることから「独立したオブジェクト間をメッセージングでつなぐ技術」という技術トピックも関数型プログラミングに当てはまることがわかります。同様に、残りの2つ「問題領域の区割りと仮想機械の区割りを関連付ける技術」と「仮想機械間の役割分担」も関数型プログラミング設計を行う際にそのまま応用できます。
つまり実は オブジェクト指向の4大技術トピックは全て関数型プログラミングに継承されているわけです。
これは「オブジェクト指向」から「関数型プログラミング」に移行しても設計工程はほとんど変わらないことを意味します。さすがに実装設計はかなり変わってきますが、そこは言語を習得すれば自動でついてきます。つまり新しい設計手法を一から学びなおすような学習コストは不要です。(モナドを内製しようなどと考えたらそれなりのインパクトがあるのでしょうけど、普通はそこまでやりませんしね。)
オブジェクト指向の設計プロセスを強要するHaskell
最後にもう一例だけオブジェクト指向設計手法が関数型プログラミングでも活きてくる例を挙げましょう。
オブジェクト指向では「オブジェクト」と「型」が一体のため静的構造設計がユルい感じで始められて取っ付きやすいのですが、それだけに「型の本質(特性)」への踏み込みが甘いまま設計しがちです。
特に小さいプロジェクトでユースケース駆動やドメイン駆動に取り組んでいると「顧客要求」を重視するあまり「コンピュータの特性」を活かすことを疎かにしたまま設計を固めてしまうケースがよくあります。私自身もそれで痛い目を見たことが少なからずありますが、これを防ぐためにダリル・クラウとイーモン・ギニーの「ユースケース導入ガイド」やヴァーン・ヴァーノンの「実践ドメイン駆動設計」では顧客要件の網羅が終わったタイミング等でコンピュータの特性を踏まえて設計を見直す工程が設計プロセスに組み込まれています。
この工程で「コンピュータの特性」の中でもとりわけ「振る舞い」に着目して再設計すると劇的な品質改善につながることが多いのですが、型とカプセル化が分かれているHaskellでは「型=構造と振る舞いを繋いだもの」について集中して考えることを強要されるため、設計を洗練される機会が強制的に開発プロセスに組み込まれることになります。これはHaskellでの開発に慣れることでオブジェクト指向設計を実践する中で研鑽された「型の特性」を活かす設計プロセスを自然に身につけられることを示唆しています。
無論、実際にはそう上手くいかないことも多いと思いますが、少なくとも型の扱いに集中して設計を見直すという体験を実際にやってみないとその効果を実感することができないので経験の差は大きいはずです。その差を経験した方ならきっと琴線に触れるものがあるのではないでしょうか?
まとめ
ここまで「オブジェクト指向のオブジェクトは仮想機械である」ことを理解することから始まり、「仮想機械の構成要素」と「オブジェクト指向の4大技術トピック」という定性的な物差しを手に入れ、「モナドが暗黙の制御構造として使われること」等の説明を経て「多態性とモナドの共通点」から関数型プログラミングがオブジェクト指向プログラミングの延長線上にあることを体感できることを述べました。
さらに「型システムについての深堀」から「4大技術トピックが関数型プログラミングにも適用される」ことへとつながり、さらには関数型プログラミング言語の代表であるHaskellでプログラミングすることに「オブジェクト指向設計のコンピュータの特性を活かす再設計工程」と同様な効果が見込めることを見てきました。
こうしてみると表面上はかなり違って見えるオブジェクト指向と関数型プログラミングが深い部分で繋がっていることが感じられます。
関数型プログラミングが世に現れた時期や昔のHaskellにはモナドがなかったこと等々、どう見てもオブジェクト指向と関数型プログラミングは直系ではありませんが、そもそも「オブジェクト指向」ですら、その前身となる「構造化プログラミング」とは大きく異なる思想でした。
ですから、かつて「構造化プログラミング」から「オブジェクト指向」へパラダイムが移った時に「構造化プログラミング」の基本的な概念はそのまま引き継がれたのと同じ様に、オブジェクト指向で培ってきた技術の多くが関数型プログラミングにおいても活かすことができるのであれば、ソフトウエア工学のパラダイムは関数型プログラミング主流に変わっていくことでしょう。
もちろんパラダイムは誰が決めるものでもないので「関数型プログラミングがオブジェクト指向の正当な後継である」というは単なる予言・予想の類(煽りも多分に含む)ですが、実際、パラダイムの軸足はオブジェクト指向から関数型プログラミングへと移りつつあります。少なくとも私はオブジェクト指向オンリーに引き返したいとは思わないし、その必要も無くなりつつあります。
というわけで、
オブジェクト指向に熟達した人々がオブジェクト指向にこだわる気持ちもわかりますが、そろそろ重い腰を上げてみてませんか?
むしろオブジェクト指向設計に真面目に取り組んできた人ほど関数の「参照透過性」、モナドの「暗黙の制御構造」、「型の特性に着目した再設計」がもたらす恩恵についてより深い理解ができるはずです。
追伸
関数型プログラミング言語では「パターンマッチ」も重要な技術トピックなのですが、今回は力不足でまとめられませんでした。なのでいずれ加筆するかも?