<style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style>
ゆびてく

Elixir から Elm の流れで、いよいよオブジェクト指向に対する懐疑心が無視できないレベルに達した2017年冬。

このエントリは Elm2 Advent Calendar 2017、2日目の記事になります。


Disclaimer: 勢いで書いてしまった後に改めて読み返してみると、Elmの中身には全く触れてないような気もしなくはない感じになってました… その辺を期待している方はブラウザ(のタブ)をそっ閉じして明日の記事にご期待下さい。


東京都港区の会社でインフラの仕事をしているフリをしながら、Elixir や Elm での関数型プログラミングに四苦八苦しつつ、Cotoami というよく分からないアプリケーションを作ったりしています。

今回は、まだ駆け出しの関数型プログラマーである筆者が、関数型プログラミングの洗礼を受けたことによって、長年慣れ親しんできたオブジェクト指向に対する見方が変わってきたという話について書いてみたいと思います。たとえて言えば、外国語を勉強することで、初めて日本語というものを客観的に見る機会を得たという体験に似ているかもしれません。

90年代からゼロ年代の中盤ぐらいまでにオブジェクト指向でプログラミングを始めた人間にとって、その考え方はプログラムデザインの共通言語のようになっていて、それ自体を疑うということには、なかなかなりづらい状況が長く続きました。Paul GrahamJeff AtwoodLinus Torvalds のような著名な人たちがオブジェクト指向に対する批判や懐疑を表明しても、「使い方の問題だよね」という感じで、オブジェクト指向そのものの問題ではないというのが多くの支持者の反応だったように思います。

あの TDD(Test-Driven Development)に対しても「Faith-based TDD」として同じ構造の批判がなされています(参考:「TDD再考 (8) – 凝集性(cohesion)とは何なのか?」)。このような議論の際によく見られる「◯◯が機能しないのは、◯◯のやり方を間違えているからだ」のような論法は、「No True Scotsman fallacy(本当のスコットランド人なら◯◯などしない論法)」だとの指摘もあります。

そもそもオブジェクト指向への批判が、関数型プログラミング界隈から行われることが多かったということもあり、その筋の人たちにとっては自明のことでも、オブジェクト指向しか知らない人たちにとっては、その指摘自体をうまく理解できないという非対称な構造がありました。

Information hiding vs. Explicitness

そのような状況の中で、オブジェクト指向への敬虔な信仰を残したまま、関数型プログラミングの門を叩いたわけですが、そこでいきなりオブジェクト指向の中心的な価値を全否定されるという事件が起こります。

オブジェクト指向では、状態というものがインタフェースの向こう側にあって、どのように実現されているか見えないようになっており、それがカプセル化、あるいは情報隠蔽と呼ばれる、複雑さを扱う技術の核心になっています。

ところが、関数型プログラミングでは状態の遷移を隠さずに、関数の入出力として表現しようとします(状態の遷移が入出力で完結している時、この関数を「純粋な関数」と呼ぶ)。

状態遷移が関数の入出力に限定されている時、プログラムの動作を把握するのは飛躍的に楽になります。一方で、オブジェクト指向ではプログラム上は簡潔に見えても、水面下に沢山の状態が隠されているので、何か問題が起きた時に状況を把握するのは容易ではありません。


The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong(関数型プログラミング言語 Erlang の作者)

「欲しかったのはバナナだけなのに、それを持ってたゴリラどころか、ゴリラがいたジャングルごとついてきた」って、分かりやすくてなかなか面白い表現ですが、それを言ったら、関数型だってバナナだけじゃ済まないだろっていう話もあるような・・・


ところが、なぜオブジェクト指向が状態遷移を隠蔽していたかと言えば、インターフェースに対してプログラミングすることによって、プログラムが実現すべき要求だけを簡潔に表現できるということがあったと思います。そうせずに、単純に状態遷移を全て入出力で表現しようとすると、プログラムはとても読み辛いものになってしまいます。この関数型特有の問題に対応するため、いくつかの関数型言語ではモナドという「本来の計算とおまけを切り離す」ための仕組みが導入されており、これはオブジェクト指向でやっていた情報隠蔽が形を変えて現れたと言えるのかもしれません。

興味深いのは、オブジェクト指向と関数型で、状態の扱いに関して言えば、全く逆の考え方になっているところです。オブジェクト指向では複雑性を扱うための核心となっていた考え方が、関数型プログラミングでは悪として扱われている。

ここでぼんやり、オブジェクト指向と関数型のハイブリッド言語ってどうなのよ? という疑念が立ち上がってくることになります。

A paradigm is a mindset

そんな思いを抱いたまま、8月に開催された Elm Tokyo Meetup #4 に参加して、そこで教えて頂いた、Elmアプリケーションのプログラムデザインについての動画を見ている内に、オブジェクト指向と関数型プログラミングはそもそも両立しないのではないかという印象はさらに強くなっていきました。

この動画を見る限り、Elmのコミュニティには、オブジェクト指向からの編入組が結構いるのではないかという印象を受けます。というのも、そこで語られていたのは、コンポーネントという、オブジェクト指向的なデザインを導入したみたけれど、どうもうまく行かなかったので、一度その辺の考え方をリセットして、もっと原理的なところからプログラムデザインを考え直してみようという話だったからです。

動画の中で、オブジェクト指向と関数型というのは、プログラミング言語の問題というよりもマインドセットの問題なのだという話が紹介されています。つまり、言語のパラダイムとは関係なく、プログラマがオブジェクト指向のマインドセットでコーディングしていれば、たとえElmのような純粋関数型の言語であっても、オブジェクト指向的にデザインされてしまうということです。

そのように考えると、マルチパラダイムの言語では、良く言えば、プログラマのマインドセットによって多彩なプログラムデザインが実現できるということになるけれど、悪く言えば、互いに相容れない複数のマインドセットを想定している場合は、単に混乱の元になるだけではないかという感じがして来ます。

Prefer duplication over the wrong abstraction

そのモヤモヤ感が強まった中で、さらに追い討ちをかけて来たのが、Cindy Sridharan 氏による「小さな関数は有害だと考えられる」というタイトルの記事です。

この記事で彼女は、一般的には名著とされている「Clean Code」に書かれているような、オブジェクト指向時代に生まれた設計指針は、むしろ過剰な構造化を誘発して、可読性や柔軟性を欠いたコードになることが多いのではないかという、いかにもその筋で炎上しそうな指摘をしています。

この指摘の背景には「そもそもオブジェクト指向が想定する抽象化が容易ではない」という問題意識があります。抽象化が容易でないのに、オブジェクト指向の設計指針には、その分割が本当に必要だと確信できるより前に、プログラムの分割を進めさせてしまうような圧力があります。

SRP(Single Responsibility Principle)

SRPでは、「1つのクラスは1つの責務を持つ」を原則とします。複数の責務を 1つのクラスに持たせないこと。1つの分かりやすい役割をクラス分割の境界 とすること。1つのクラス内に入る要素(属性や操作)が、1つの目的に向かっ て凝集していること。これが原則です。

クラスに変更が起こる理由は、一つであるべき。
A class should have only one reason to change.

– ソフトウェア原則[3]

 

ISP(Interface Segregation Principle)

クライアントは自分が使わないメソッドに依存することを強制されない。
Clients should not forced to depend on methods that they don’t use.

クライアントが本当に必要としているインターフェイスのみ が、クライアントから見えるべきで、他のメソッドには依存したくない。依存 を最小にして、変更の伝播を最小限に食い止めたい。Segregationとは分割、分 離、という意味です。つまり、ISPは「インターフェイスをクライアント毎に分離しよう」という原則なのです。

– ソフトウェア原則[4]

この問題について、Elmの作者である Evan Czaplicki 氏も同じような話をしています。上の動画と同じ「Elm Europe 2017」にて行われた発表によれば、

JavaScript での開発では、モジュールを細かく分けて、小さなファイルを沢山作る傾向があるが(”Prefer shorter files.”)、何故そのようなことになるかと言えば、

1) 一つのモジュールが大きくなると、その内部で何か想定外のことが起こる可能性が高くなる(想定外の状態共有や変更)
2) Static type のない JavaScript では、リファクタリングのコストが高くなるので、早い段階で分割を進めてしまう

という理由があるからではないかと指摘しています。Elmでは、1) に対しては、副作用がない純粋関数型であること、2) に対しては、強力な型システムがあることによって、これらの懸念を払拭できるため、先走りのモジュール分割を避けることが出来るというわけです。実際に、Elmでは一つのファイルやモジュールが大きくなることについて、他の言語(特にオブジェクト指向言語)よりも寛容であるということがよく言われています。

そもそも適切な抽象化が難しいのに、その抽象化が有用であるという証拠が揃わない内に分割を進めてしまうと、より不適切な分割をしてしまう可能性が高くなってしまいます。先走って分割したモジュールが、後々の状況変化に対応できなくなって、例外条件に対応するコードが増えて行き、そしてスパゲッティ化していく過程というのはシステム開発の現場で働く多くの人が目撃しているのではないでしょうか。

duplication is far cheaper than the wrong abstraction, and thus to prefer duplication over the wrong abstraction

「間違った抽象化よりも、コードの重複の方を好む」ということで、長らく信奉されてきたDRY原則への挑戦がここでは行われています。

Leaky abstraction

そして、偶然なのか何なのか、これも同じ8月に、オブジェクト指向における抽象化がなぜ難しいかというのを良く表している大変興味深い記事を見つけることになります。

カプセル化することが自己目的化していて、何のためにカプセル化するのかという視点が極めて希薄なことです。その結果、「うまいカプセル化」「まずいカプセル化」の区別がない、という状況に陥っています。

本来は、値引き判定のロジックをどのオブジェクトに配するかを決めるにあたって、どのような知識を隠蔽すべきか、あるいは裏返して言えば、どのような知識は開示して構わないかという点に思いをめぐらすべきでした。

解決策は、「データとロジックを一体に」という、どちらかというとゲームのルールのような具体的で単純なルールから視点を引き上げ、「情報隠蔽(=知識隠蔽)」のような、より本質的な、目的志向的な設計原則に立ち帰って考えることです。

この記事の要旨は、増田亨氏の「現場で役立つシステム設計の原則」という書籍で紹介されているオブジェクト指向のコード例について、「データとロジックを一体に」というオブジェクト指向の表面的なルールに囚われ過ぎて「何を隠蔽して何を表に出すのかという設計判断」を蔑ろにしているという指摘です。

しかし、この記事を読んで個人的に思ったことは、設計判断の根拠となる「スコープが適切でない」ということを、後から文脈をズラすことでいくらでも言えてしまうというのが問題の本質じゃないか、そこに「データ・ロジック一体型設計」の限界があるということなのではないか、ということでした。

うまく抽象化したつもりでも、どこかに必ず漏れが出てきてしまうという話は、「Joel on Software」の「The Law of Leaky Abstractions」という、2002年に書かれた記事に出てきます。

TCPプロトコルが、下位のネットワークをうまく抽象化しているように見えて、実際はいくつかの例外ケースで、その隠蔽しているはずのネットワークの存在が漏れて出してしまう(Abstraction Leak)。そうなった時にかかるコストというのは、抽象化がなかった時よりも高くついてしまう可能性があります。隠蔽された部分の知識も結局のところは必要なのだとなれば、抽象化された部分と隠蔽された部分の両方の知識が必要になるからです。

Objects bind functions and data structures together

オブジェクト指向の問題点を指摘する場合、一番厄介なのは、オブジェクト指向に定まった定義がないという事実です。このブログでは以前、オブジェクト指向の歴史を遡って、あれってそもそも何だったのかということについて検討したことがあります。

歴史的な経緯から言えば、オブジェクト指向を発案したアラン・ケイ氏が言うところの「メッセージング」が、オブジェクト指向の本質だということなりそうですが、一般に普及した「オブジェクト指向言語」と呼ばれるもので、メッセージングをサポートしているものはほとんどありません。メッセージング、あるいはそれが実現する動的結合(late binding)だけを考えると、それは今、オブジェクト指向と呼ばれるものよりも遥かに広い範囲で利用されていますし、実際にはオブジェクト指向言語じゃなくても実現できることを考えると、C++ をきっかけに流行した「抽象データ型」を起源とする流れが、一般的に認識されているオブジェクト指向だと考えて差し支えなさそうです。

ちなみに、オブジェクト指向信者の反論を「No True Scotsman fallacy」だと指摘した Lawrence Krubner 氏によれば、一般的にオブジェクト指向の強みだと思われているほとんどの要素はオブジェクト指向固有ではなく、オブジェクト指向固有の強みなど、実際には一つもないそうです。

オブジェクト指向固有でないものを除外していくと、最後に残るのが「データとロジックを一体に」という先ほどのルールです。そして、どうもこのルールがオワコンになりつつあるのではないかというのが、この約1年間、Elmでプログラミングをしていて実感するようになったことです。

関数型プログラミングをしていると、データとロジックが分かれていることのメリットを実感する機会が度々あります。アプリケーションにはアプリケーション固有の複雑さというものがあって、それらは多くの場合、必要な複雑さである場合が多いような気がしています。オブジェクト指向では、データとロジックを一緒にしなければならないという制約のために、それらの例外的だと思われるケースを捨象して、現実に即さないモデルに(強引にでも)落とし込むことになります。必要な複雑性を無理に捨象しようとするから、Abstraction Leak が起こります。

オブジェクトに関数が結びついているからこそ「このメソッドはこのオブジェクト構造を処理するためのものだ(他の用途には使えない)」という風に専門化できていたんであって、データと関数を個別のものと扱う以上は、「この関数はこのオブジェクトだけを扱う」という前提を置けないのです(当たり前です)。あるオブジェクトと別のオブジェクトが、型であったりクラスであったりが異なったとしても、関数は、そのオブジェクトが、関数の処理できる構造であれば、処理できるべきなんです。関数とオブジェクトが独立しているというのはそういう意味であるべきです。であれば、すべての関数にまたがるような、共通の汎用データ構造があって、すべてのデータはその汎用性を担保してたほうがいい。 Clojureの世界観 – 紙箱

Oscar Nierstrasz 氏が、彼のオブジェクト指向批判の中で、「オブジェクト指向とは、つまりモデリングなのだ」と喝破していますが、複雑な事象を分かりやすい用語(ターム)の集合に落とし込めるという先入観が、アプリケーションレベルの複雑性を扱う時に明らかな障害となって現れるケースが多くなっているような気がします。

メッセージングが今のオブジェクト指向と関係ないのだとすれば、「データとロジックを一体に」がオブジェクト指向の核心になりますが、そうだとすれば、オブジェクト指向自体がオワコンだという結論になってしまいます。そして、それはどうもそうっぽいという感じがしているのです。

Solve problems of its own making

ここからは少し余談になりますが、以上のような気づきを得た上で、過去のオブジェクト指向批判の文章を読むといちいち首肯できることが多くて困ってしまいます。

オブジェクト指向というのは、オブジェクト指向にしかない問題を作り上げて、それを解決するためのツールを作るというマッチポンプ的なことをしてお金を稼いでいるという話があります。

If a language technology is so bad that it creates a new industry to solve problems of its own making then it must be a good idea for the guys who want to make money. Why OO Sucks by Joe Armstrong(さっきのバナナの人)

あるいは、オブジェクト指向ではそもそも過剰な複雑性を作り込んでしまう傾向があるという批判があります。何故かといえば、オブジェクト指向には、インターフェースに対してプログラミングするという考え方があるので、プログラムは自然にレイヤー構造になっていくからです。もう古典と言っても良いかもしれない、Martin Fowler 氏の「リファクタリング」にも、

「コンピュータサイエンスは、間接層(indirection)を設けることであらゆる問題が解決できるという信念に基づいた学問である。― Dennis DeBruler」

とあったりしますが、

しかし、間接層はもろ刃の剣であることに注意しなければなりません。1つのものを2つに分割するということは、それだけ管理しなければならない部分が増えるということなのです。また、オブジェクトが、他のオブジェクトに委譲を行って、その先もさらに委譲を繰り返すような場合、プログラムが読みにくくなるのも事実です。つまり間接層は、最小限に絞り込むべきなのです。

とは言え、実際には過剰なレイヤー構造になっていることが多いような気がします。アジャイルという考え方が出てきて、「Just enough」や「YAGNI」なんてことが言われるようになりましたが、今を思えば、これはオブジェクト指向の側にそもそも過剰な複雑性を生む性質があったために、わざわざ言わなければならなくなったことのようにも思えてきます。

オブジェクト指向特有の問題として指摘されている中で、ああこれはと思ったのは、オブジェクトをどう作るかという問題、すなわち Dependency Injection の問題です。

OOP was once seen as the silver bullet that was going to save the software industry. Nowadays we need a silver bullet to save us from OOP, and so, we are told (in the next paragraph), the Inversion of Control Container was invented. This is the new silver bullet that will save the old silver bullet. Object Oriented Programming is an expensive disaster which must end | Smash Company

これはまさにオブジェクト指向にしか存在し得ない問題を、比較的大掛かりに解決しようとした例の代表だと言えそうです。今改めて考えると、この枠組みには二つの問題があって、一つはインタフェースベースのポリモーフィズムの問題(あらかじめ想定したインタフェースの範囲の柔軟性しか持てない)、そしてもう一つは、本当に複数の実装を必要とするケースがどれだけあるのか? という問題です。後者は仮に統計が取れれば面白い数字が出てきそうですが、少なくともテスト時にモックオブジェクトに置き換えられるという主張は、テスト容易性はすなわち良いデザインではないと、Ruby on Rails の作者である David Heinemeier Hansson 氏に批判されています。

最後に

なんか、「もうやめて!オブジェクト指向のライフはゼロよ!」みたいな感じになってしまいましたが、これは完全に Elm のせいです。