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 のせいです。

Elm沼とKubernetes沼で交互に溺れているうちに1月が終わっていた

ElmCotoami のユーザーインタフェースを作るために選択したプログラミング言語で、そのデザインは JavaScript 界で一世を風靡している Redux の原型になったと言われている。その原型を体験してみたいという単純な好奇心だけで Elm を選択し、分かりやすいチュートリアルに感心したのも束の間、実際にアプリケーションを作り始めると、入り口ではあんなに優しかった Elm の顔がみるみる般若のようになり、気がついたら底なしの泥沼に足を取られていた。

Elm のような、いわゆる純粋な関数型と呼ばれる環境では、言語からコンピューターを直接操作することが出来ない。出来るのはデータの変換だけだ。この変換がいわゆる「関数」で、関数型でプログラマーに許されているのは関数を書くことだけである。しかし、コンピューターを操作出来ないと何も出来ないのでどうするのかと言うと、コンピューターへの命令をデータとして表現して、それを返す関数を作って環境に渡すみたいなことをする。

関数型にはこのような制限があるので、従来の命令型言語でプログラムを書く場合に比べて、かなり回りくどい書き方をしなければならない。しかしその代償として、プログラムは必ず「データの変換」という形に落とし込まれるので、実行時に意図しない動作をすることが格段に少なくなり、デバッグやテストも容易になるというわけだ。

関数型の回りくどさに加えて、Elm には厳密な型システムがある。実は今、開発スピードを減退させている最大の原因はこの型システムである。代数的データ型とか、今まで遭遇したことのないコンセプトに純粋な感動を覚える一方、曖昧さを許さない型システムのおかげで、型を合わせるためにどうすれば良いかというのを考えてるだけで膨大な時間が過ぎて行く。

動的型が、書く時にいかに楽をするか(少ない記述でいかに多くを実現するか)を主眼に置いているとすれば、静的型は、書いた後にいかに楽をするか(起こりうることを明示的に示しておく)ことに主眼を置く。しかし、動的型でも、曖昧に書いて楽をしつつも、危ないと思うところは慎重に書いたり、テストでフォローしたりするわけなので、単純に上のような図式が当てはまるとも思えず、どちらが良いと判断するのはなかなか難しい。動的型の言語であれば、場所によって手綱を締めたり緩めたり出来るのが、静的型だと一律で同じような書き方をしなければならない、というのがなかなか辛いところである。

まだ言語自体に慣れてないということもあるし、これから開発が進んで、規模が大きくなり、複数人で連携するようになったら関数型や型システムの恩恵を実感出来るのかもしれないなと思いつつ、最初の敷居はやっぱり高かったということだけここに記録しておく。

一方の、Kubernetes 沼の方はと言えば、こちらは Cotoami で試行錯誤した成果を仕事の方にフィードバックしたいということで、プロダクションへの導入を目指して色々と準備しているため、Elm よりもより深い深い泥沼となっている。Elm の場合はとりあえずユーザーインタフェースが動けばなんとかなるのに対して、インフラの場合はその辺のごまかしが効かないので、あらゆる側面から検証を行わなければならず、人材の不足も手伝ってかなり余裕のない状況に陥ってる今日この頃(もし、同じように Kubernetes で試行錯誤している方がいたら、情報交換したいなあと思うのですが… @marubinotto までご連絡お待ちしております(切実))。

Kubernetes は未来のインフラだとの意を強くする一方(やっと10年ぐらい前のGoogleに追いついただけとも言える)、アジャイルでない組織に導入してもメリットはあまりないだろうなとも思う。Kubernetes は単なるインフラというよりも、サービスがどのように開発・運用されるべきかという思想が強烈に埋め込まれた環境だと言った方が良いかもしれない。

Kubernetes によって、いわゆるデリバリーサイクルは極限まで短縮され、システムの柔軟性もかつてないレベルで実現出来るようになる。しかしその一方、運用やモニタリングにかかるコストは従来より高いように感じられるし、Kubernetes 自体も高速で進化していくので、そこにキャッチアップ出来る人材も確保する必要がある。例えば、運用において、MTTR (Mean-Time-To-Recovery) よりも、MTBF (Mean-Time-Between-Failures) の方を重視するような組織だと、なかなかこの機動性をメリットだと感じるのは難しいだろう。

既に開発が落ち着いていて安定運用されているサービスを Kubernetes に移行するメリットは、おそらくほとんどない。基本的には開発の初期から導入するのが望ましい。Cotoami はリスクのないオープンソース開発なので、その辺の事情で迷う必要がなく、一番始めのハリボテアプリの段階から Kubernetes で本番 ( https://cotoa.me/ ) の運用を始めて、自動デプロイなどの仕組みも整備出来た。

去年まで、ウチのチームでは、AMI (Amazon Machine Image) のように VM 上で動くホストがパッケージングの単位になっていた。パッケージを作るのも、パッケージをデプロイするのもうんざりするほど時間がかかる。それが Kubernetes/Docker によって、ホストの中で動く一つ一つのプロセスがパッケージングの単位となり、それらの小さな部品を組み合わせてホスト(Kubernetes では Pod と呼ばれている)を作り、それらをまた組み合わせて大きなWebサービスを作るという形に変わり、一度の更新も最小限で高速、しかも無停止ということで、新しい時代の到来を感じずにはいられない。

Elixir/Phoenix と Elm による関数型 Web 開発環境の構築

前回は、Cotoami のアーキテクチャについて、コレオグラフィ型を採用するという話を書いた。しかし、開発の最初からコレオグラフィを前提にした構成にするのはスモールスタートとは言い難いので、まずは核となるWebアプリケーションを作るところから初めて、徐々にイベント駆動の箇所を増やしてく感じで進めたい。

このWebアプリケーションを実装する環境として選んだのが、Phoenix FrameworkElm である。両方とも関数型の言語なので、Webアプリケーション全体を関数型の枠組みで実装することになる。

Elixirの強みについてはゆびてくで何度か触れているのでここでは割愛するが、Elm を選択したのは何故だろうか?

大きな要因としては、Elmアプリのアーキテクチャを参考にデザインされたという JavaScript のライブラリ Redux での開発経験が挙げられる。その過程で、複雑化するフロントエンドを実装する技術として、全てのビジネスロジックを「変換の連鎖」へと落とし込む関数型の有効性を実感した(参考: 関数型つまみ食い: 関数型とはプログラミング言語ではなく、プログラムデザインの問題であることに気づく | ゆびてく)。Elm の場合は、Redux では冗長になりがちだったこの仕組みを簡潔に表現出来る上に、Static Typing があるというのも大きなアドバンテージだと考えた。

エディタ上で即座にフィードバックを受けることが出来る
プログラムの誤りについて、エディタ上で即座にフィードバックを受けることが出来る

Phoenix と Elm の相性については、最近 Elm 側で Phoenix のサポートが入ったというのが明るい材料ではあるが… こればかりは試してみないと分からない。


[2016/12/09追記]

素晴らしいツッコミを頂く。


 
以下に Phoenix/Elmアプリケーションのひな形を作るまでの手順をまとめてみた。

 

関連ツールのインストール

Node.js

以下を参考に nvm をインストールする。

creationix/nvm: Node Version Manager – Simple bash script to manage multiple active node.js versions

  • Phoenixのサイトに「Phoenix requires version 5.0.0 or greater.」とある。

筆者の環境:

$ node -v
v5.4.1

 

Elixir

Installing Elixir - Elixir

Mac OS X で Homebrew を利用している場合。

$ brew update
$ brew install elixir

筆者の環境:

$ elixir -v
Erlang/OTP 19 [erts-8.0.2]  [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.3.4

 

PostgreSQL

標準構成の Phoenix が利用するデータベース。環境によってパッケージも様々なのでインストール方法については割愛。データベースを使わないのであれば省略可。

以下のコマンドでデータベース一覧が取得出来ればデータベースのスタンバイは出来ている。

$ psql -l

筆者の環境:

# SELECT version();
                                                              version                                                              
-----------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 9.4.0 on x86_64-apple-darwin13.4.0, compiled by Apple LLVM version 6.0 (clang-600.0.56) (based on LLVM 3.5svn), 64-bit
(1 row)

 

Phoenix

Installation · Phoenix

$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez

筆者の環境:

$ mix phoenix.new -v
Phoenix v1.2.1

 

Elm

インストーラが用意されているので簡単。

Install · An Introduction to Elm

筆者の環境:

$ elm -v
0.18.0

 

Phoenix/Elm アプリケーションを作る

Phoenixアプリのひな形を作る

$ mix phoenix.new cotoami

依存関係の取得とデータベースの作成。

$ cd cotoami
$ mix deps.get
$ mix ecto.create   # PostgreSQLを使わなければ省略可
$ npm install

アプリを起動してブラウザでチェックしてみる。

$ mix phoenix.server

http://localhost:4000 にアクセスすると「Welcome to Phoenix!」のページが表示される。

 

elm-brunch をセットアップする

Phoenix に標準で付いてくる Brunch というJavaScriptのビルドツールがあるのだが、elm-brunch という Elm をビルドするための拡張があるのでそれをインストールする。

$ npm install --save-dev elm-brunch

brunch-config.js に elm-brunch の設定を追加。以下の二カ所を修正。

  1)
    ...
    watched: [
      "web/static",
      "test/static",
      "web/elm"
    ],
    ...

  2)
  ...
  plugins: {
    elmBrunch: {
      elmFolder: "web/elm",
      mainModules: ["App.elm"],
      outputFolder: "../static/vendor"
    },
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/web\/static\/vendor/]
    }
  },
  ...

 

Elmアプリのひな形を作る

$ mkdir web/elm && touch web/elm/App.elm
$ cd web/elm
$ elm package install elm-lang/html

App.elm の内容を以下のように編集。

module App exposing (..)

import Html exposing (Html, text)

main : Html msg
main =
  text "Hello Cotoami!"

 

ElmアプリをPhoenixアプリに配置する

Phoenixアプリのファイルをそれぞれ以下のように編集。

web/templates/layout/app.html.eex

web/templates/page/index.html.eex

<div id="elm-container"></div>

web/static/js/app.js に以下の二行を追記:

const elmDiv = document.querySelector("#elm-container")
const elmApp = Elm.App.embed(elmDiv)

これで準備は完了。ブラウザをリロードすると「Hello Cotoami!」と表示される。さらには、App.elm の内容を編集して保存すると、ブラウザが自動的にリロードされて即座に変更を確認出来るようになっているはずだ。

参考: Setting up Elm with Phoenix – Medium

組織やチームの話を追いかけると、プログラミングは基礎教養なんだって話に辿り着いてしまう件

まさにそんな感じの話がInfoQで紹介されていた。

関数型つまみ食い: 関数型とはプログラミング言語ではなく、プログラムデザインの問題であることに気づく

関数型プログラミングに精通していないプログラマにとって、「関数型」という言葉でまず思い浮かべるのは、Haskell や Erlang,Scala のような関数型言語や、Immutableなデータ構造、高階関数、ラムダ式、モナドと言った関数型プログラミングの道具立てだったりすることが多いのではないだろうか?

前回登場したゲームプログラマの James Hague 氏も、筆者と同じく、キャリア半ばで関数型の世界に足を踏み入れた越境者の一人だったが、その彼が、関数型言語 Erlang でのゲーム開発を経験して気がついたことがあると言う。それは、アプリケーション開発に純粋な関数(副作用のない関数)というコンセプトを導入することは、いわゆる関数型言語や関数型プログラミングとは全く関係ないということであった。

関数型というコンセプトの要は、ビジネスロジックを「データの変換」と捉えることにある。

functional1

関数というのは要するに「変換のルール」のことであるが、この関数が文字通り変換のルールのみを定義し(副作用がなく)、そして変換対象のデータを直接編集せずに、常に変換後のデータに丸ごと置き換えて行けば、システムの状態変化は常にアトミックに行われることになる。

functional2

このような形で、システムの状態遷移をシンプルにすることによって得られるメリットの大きさに、筆者の場合、関数型言語というよりもむしろ、Redux というフロントエンドのフレームワークを学ぶ中で気づかされた。

Redux はフロントエンドの状態管理を行うためのフレームワークである。オブジェクト指向に慣れているとアプリケーションの状態は個々のオブジェクトに分散するのが当たり前であるため、何故状態だけを独立して管理する必要があるのか、今ひとつピンと来ないこともあるかもしれない。GUIとオブジェクト指向というのは歴史的にも結びつきが強いため、この領域に関数型を適用するというのはどういうことなのか、これは筆者にとっても長年の疑問であった。

Redux は React というフレームワークとペアになっている。React はユーザーインタフェースをレンダリングするためのテンプレートエンジンで、入力データをテンプレートに当てはめて結果を出力するという、まさに関数としてデザインされている。そのため、テンプレート内で状態管理をすることは推奨されていない。状態は外部で管理して React 側に提供する必要がある。その状態管理を担うのが Redux である。

redux

Redux の仕組みは、上に書いたような関数型の状態管理そのままである。筆者にとって重要だったのは、これが JavaScript という、比較的慣れ親しんだ言語の上に構築されたフレームワークであったことだ。これによって「ああ、つまり関数型というのはフレームワーク(デザイン)なのだ」という気づきを得ることが出来た。

関数型デザインによって実現するシンプルな状態遷移のメリットは、Redux のプラグイン redux-logger によって出力されるログを見ると分かりやすい。

redux-logger

上のように、Redux で状態を管理していれば、そこで発生する全ての状態遷移は、

  1. prev state
  2. action
  3. next state

の繰り返しによってアトミックに行われる。何か問題が発生した時は、開発者自身があちこちにログを仕込んだりデバッガを活用せずとも、この状態遷移の内容を見れば何が起きたのか一目瞭然である。

Redux のドキュメントでは、関数型(functional)という言葉を出来るだけ使わずに説明を試みているようである。しかしながら、Prior Art にあるように、Redux の直系の先祖はブラウザで動く(ほぼ)純粋な関数型の環境 Elm であり、Elm は Haskell をベースにデザインされていることから、Redux は純粋関数型の思想を純粋関数型でない言語の上に実現したフレームワークだと言って差し支えないだろう。

関数型つまみ食い: モナドが難しいと思われている理由

(「モナドについて書かれたものを読む度に自分で書いてしまいたくなる。同じ過ちを繰り返すだけだから止めた方がいいんだけど。」という話を受けて、)

世の中に溢れる粗悪なモナド入門のせいで Philip Wadler氏の論文が埋もれているからモナドが難しいと思われるんだ。

ほほう、その論文を読めばモナドとやらを簡単に理解出来るのかな、と思いつつ読んでみると、

全然簡単じゃないんですけど…

でも、この論文でモナドの意義と基本的な仕組みはぼんやりと見えたような気もしなくはない。その理解をここに簡単にまとめておくことで、同じ過ちを繰り返してみることにしよう。

まずモナドを理解するためには、関数型プログラミングにおける、純粋な関数とそうでないものの区別をきちんとさせておく必要がある。

純粋な関数とは何か?

そもそも関数とは何か? それは、入力を出力に「変換」するものである。

関数: 入力 =(変換)=> 出力

至ってシンプルである。関数型プログラミングでは、この関数を組み合わせて、変換の連鎖としてアプリケーションを表現する。

さて、この関数が以下の二つの条件を満たすとき、その関数を「純粋」な関数と呼ぶ。

  1. 同じ入力に対しては常に同じ出力を返すこと
    • 例えば 1 を渡したら 2 が返ってくる関数があったとして、どのようなタイミングや事前条件で関数を実行しても 1 => 2 という同じ結果になること、そしてそれが他の入力パターンについても同様に成り立つこと。
  2. 関数は入力を出力に変換するものだと説明したが、その変換以外のことが一切起こらないこと
    • この変換以外の出来事を「副作用」と呼ぶ。

純粋な関数のメリットと痛み

Elixir試飲録 (4) – オブジェクト指向と関数型の違い」で触れたように、オブジェクト指向ではシステムの構成要素であるオブジェクトの「インターフェース」にフォーカスすることによって、システムの表現を単純化し、さらには疎結合による柔軟性を獲得していた。しかし一方で、インターフェースに現れない内部的なデータの流れは隠蔽されるため、インターフェースだけでシステムの(厳密な)状態遷移を把握することは困難になる。

object

少ない記述で多くを実現しようとするオブジェクト指向に対して、関数型、特に純粋関数型では、原理的に関数(入出力の変換)として書かれたことしか起こらないので、不測の事態が起こりづらく、より信頼性の高いシステムを構築しやすいと言える。しかし、逆に言えば、全てのデータの流れを入出力の変換として明示的に表現しなければならないので、時にはコードが冗長になってしまうケースがあるだろう。

function

この関数型の欠点とも呼べる冗長さを軽減するために導入されたのが「モナド」と呼ばれるコンセプトである。

モナド概念の導入

「関数型の欠点とも呼べる冗長さ」とは具体的にどのようなものだろうか? 以下のような例で考えてみる。

ここに「倍返し」という関数がある。名前の通り、受け取った数値を倍にして返す関数である。

倍返し: 数値 =(変換)=> 数値

この関数に 1 を渡すと 2 が返り、2 を渡すと 4 が返る。

この関数が「受け取った数値を倍にする」ことだけに専念しているとき、この関数は純粋であると言える。

しかし、世知辛い現実世界では、単に「倍返し」の純潔を守っているだけでは生き残って行けない。色んな要求が舞い込んでくる。例えば、倍返しに失敗したらどうしてくれるんだ?、だったり、倍返ししたことをちゃんと記録しておけよ、等々。そこで以下のような要求があった場合、どのように対応すれば良いだろうか?

  1. エラーが発生したら、それをきちんと知らせて欲しい
  2. 何回「倍返し」したのか記録しておいて欲しい
  3. 倍返しする度にログを吐いて欲しい

もし、関数が純粋でなくても良いなら、話は簡単である。エラー処理については、出力以外にも「例外」という経路を設けて、そこからエラーを知らせればよい。実際に多くのプログラミング言語ではこの方式を採用している。倍返し回数の記録についてはグローバルな変数を用意してそれをカウントアップして行けばよいし、ログについては深く考えることなく、普通にログを吐けばいい。

しかし、関数を純粋に保ったまま、つまり純粋な関数のメリットを維持したまま、このような機能を実現するためにはどうすればよいだろうか? 単純に考えると、出力にそのような情報を追加するしかない。

倍返し1: 数値 =(変換)=> 数値, エラー
倍返し2: 数値 =(変換)=> 数値, 回数
倍返し3: 数値 =(変換)=> 数値, ログ

エラーを投げたり、回数を記録したり、ログを吐いたりするのは「副作用」に当たるので、関数の純粋性を守るためには、これらの処理は「予定」として、関数の出力に含めて表現することになる。

これは明確さという観点から言うととても分かりやすい。関数の定義を見ればどのようなことが起こるのかが一目瞭然だからである。しかし一方で、関数の定義に、本来の処理「倍返し」以外の情報が紛れ込んでくるし、要求が増える度にいちいちこのような関数のバリエーションを増やして行かなければならないのだろうか?

そこで、このような「入出力に紛れ込む副作用」を本来の処理から隠蔽して冗長さを軽減するために導入されたのが、いわゆる「モナド」である。

「倍返し」の例を一般化すると、

倍返し: 数値 =(変換)=> 数値, おまけ

となるが、この関数に、どのような「おまけ」があるにしろ、そのことを意識せずに一番純潔な

倍返し: 数値 =(変換)=> 数値

として扱えれば便利である。そのために必要となるのが以下の仕組みである。

  1. まず前提としておまけ付きの出力: [本来の出力, おまけ]
  2. 本来の出力をおまけ付きに変換してくれる仕組み: 出力値 => [本来の出力, おまけ]
  3. おまけ付きの値を、そのままおまけ無しの値として関数に渡せる仕組み: [本来の入力, おまけ] => 本来の入力

この三つの組み合わせを「モナド」と呼ぶ。これらの道具立てがあれば、関数本来の計算がおまけに埋没することなく、かつ関数の純粋さを保つことができるというわけである。


[2017/05/18追記]

久しぶりにこの記事を読んでみて、もう少し説明を加えればモナドに対する理解がよりクリアになるような気がしたので、試しに書き加えてみる。

モナド三兄弟

関数型プログラミング(特に純粋関数型)は、システムの状態遷移を余すところなく関数の入出力として表現することで、システムの動きを明確にしようとする。全てを入出力として表現するので、ビジネスロジックにとって重要な「本来の入出力」以外の「おまけの入出力」がどうしても必要になってくる。

つまり、関数と関数の間を受け渡されていくデータには、常に何らかのおまけが含まれている可能性があるということだ。そのおまけは、例えば、処理に失敗した際のエラー情報だったり、値が存在しないかもしれないという可能性だったり、あるいは入出力には現れない文脈的なデータだったりする。このおまけの応用範囲がとても広いというのが、モナドのつかみどころのなさの一因になっていることは間違いなさそうだ。

しかし、関数の方はこのおまけを解釈するようにはできていないかもしれない、あるいはむしろ、特定のおまけを想定して関数を書いてしまうと、その関数の汎用性が大きく損なわれてしまうので、できれば本来必要な計算だけに集中したい。

そこで、おまけを想定しない関数を、おまけ付きの入出力を持つように変換するアダプター的な仕組みがあれば、本来の計算とおまけを切り離せる。この仕組みがモナドである。

より正確に言うと、この変換には三つのタイプがあり、その中の一つがモナドと呼ばれている。

具体的には、

a) 値 =(変換)=> 値  (全くおまけを想定してない関数)
b) 値 =(変換)=> 値, おまけ (おまけ付きの値を返す関数)
c) (値 =(変換)=> 値), おまけ (関数自体におまけが付いてる!?)

のような関数を、あたかも、

値, おまけ =(変換)=> 値, おまけ

のように扱えるようにしたい。この変換に対応するアダプターをそれぞれ、

a. Functor (ファンクタ)
b. Monad (モナド)
c. Applicative (アプリカティブ)

と呼ぶ。

ところで、上記三つの名前を見て、それぞれの機能が想像できるだろうか? これらは数学の「圏論 (category theory)」という分野由来の言葉で、そのままで意味をイメージできる人はかなりの少数派ではないかと思う。この名前の問題も、モナドが難しいというイメージを与える一因になっている。

そこで、以下のようにもっと分かりやすい名前で呼ぶようにしたらどうかという提案がなされているようだ。

  • Functor -> Mappable
  • Monad -> Chainable/Joinable/FlatMappable
  • Applicative -> Zippable

厳密にはこれらの語が意味するところとは異なるので(圏論的なニュアンスが抜け落ちるので)適切ではない、というのが専門家の見解であるようだが、これらの概念を理解する際の参考になることは間違いない。

まず Functor の Mappable という別名はとても分かりやすい。おまけ付きの値でも、[値 =(変換)=> 値] の関数でマッピングできるようにしてくれるものが Mappable というわけだ。

Functor で最もポピュラーだと思われるものが、値が「繰り返し」というおまけを持つ場合(これはつまりコレクションのこと)に対応する Functor だ。この Functor は、まさに map という名前で、多くの言語に導入されてお馴染みになった。コレクションを別のコレクションに変換するあの関数だ。この map に渡される変換用の関数は、値がコレクションの中に入っているという「おまけ」を気にすることなく、単に一つの値を別の一つの値に変換することだけに集中することができる。「本来の計算とおまけの分離」である。

Monad の Chainable も分かりやすい。

値 =(変換)=> 値, おまけ

という関数をつなげて (Chaining) 使いたい場合に、入力を変換するものが Chainable になる。

Applicative – 複数の入力値を取る関数をおまけ付き入出力に対応させる

Applicative はちょっとややこしい。これは、複数の入力値を取る関数に Functor 的な変換を行う際に使う。これまでの話では、あくまで一つの入力値を取るケースだけを想定して話をしてきた。

例えば、以下のように二つの入力を取る関数があるとする。

A, B =(変換)=> C

関数型プログラミングをサポートする言語の一部には、関数の「部分適用」という仕組みがある。これは複数の入力を受け取る関数があるときに、その中の一つの入力だけを渡して、その入力を受け取った状態の新たな関数を作る機能だ。

具体的に見てみよう。上の関数に、一番目の入力、A だけを与えると、入力を B 一つだけ受け取り、C を返す関数ができる。

A
↓
A, B =(変換)=> C
↓
B =(変換[A入力済み])=> C

さて、この部分適用という仕組みを踏まえて、二つの入力を取る関数におまけ付きの値を渡すためにはどうすれば良いだろうか?

まずは一つ目の入力値を Functor で渡してみよう。

A, おまけ
↓
A, B =(変換)=> C
↓
(B =(変換[A入力済み])=> C), おまけ

Functor は、入出力をおまけ付きに変換するので、部分適用して出来上がった関数がおまけ付きになってしまった。

というわけで、ここで Applicative の登場である。

すでに書いたように、Applicative は、

c) (値 =(変換)=> 値), おまけ

値, おまけ =(変換)=> 値, おまけ

に変換してくれる。つまり、

B, おまけ
↓
(B =(変換[A入力済み])=> C), おまけ
↓
C, おまけ

となって、最終的に欲しかった「C, おまけ」を手に入れることができた。

つまり、これら(Functor と Applicative)を組み合わせると、

A, おまけ and B, おまけ
↓
A, B =(変換)=> C
↓
C, おまけ

という感じで、複数の入力値を取る関数をおまけ付き入出力に対応させることができる、というわけである。

Elixir試飲録 (4) – オブジェクト指向と関数型の違い

古いブックマークを漁っていたら、オブジェクト指向と関数型の違いに関する面白い記事を見つけた。

記事は Michael Feathers 氏の興味深いツイートから始まる。

  • 試訳
    • オブジェクト指向は、可変部分をカプセル化(隠蔽)することによって、コードを分かりやすくする
    • 関数型は、可変部分を最小化することによって、コードを分かりやすくする

筆者なりに解釈すれば、オブジェクト指向はインターフェースにフォーカスし、可変部分を捨象することによって複雑なシステムを単純化し、関数型は副作用のない関数を組み合わせることによってシステムの可変部分を最小限に保ち、システムの動的な性質を掴みやすくしようとする。

オブジェクト指向の「可変部分を捨象する」という考え方がシステムを単純化する一方、インターフェースの裏側で起こることは予測し辛くなる。インターフェースの「契約」に現れない状態変化、極端な例を挙げれば「重要なデータを削除しないこと」など、それら全てについて自動テストを用意するのは現実的ではないため、オブジェクト指向のシステムが提供出来る信頼性には自ずと限界があることになる。

そして興味深いのは、オブジェクト指向の上に関数型の仕組みを乗っけるのは止めた方が良いという主張だ。この記事を書いた John D. Cook 氏は、関数型のメリットを享受したいのなら、まずは関数型だけに集中し、プログラムの大きなパーツ(モジュールなど)を管理する時にオブジェクト指向的な枠組みを利用すれば良いのではないかと書く(「functional in the small, OO in the large」)。

コードの中に、関数型の部分とそうでない部分が混在してしまうと、全体としては関数型の恩恵を受け辛くなるのだろう。この話題はこれからもちょくちょく出てくると思うが、Scala と Elixir の比較という観点からも興味深い指摘である。Scalaは基本的にオブジェクトから出発して関数型の流儀を徐々に入れて行くの対し、Elixirはまず関数型から出発する。そして、そこから関数をグルーピングするための Module や、Moduleにインターフェースの仕組みを導入するための Behaviour、そしてポリモルフィズム(多態)のような仕組みを実現する Protocol などが出てくる。

James Hague 氏は、100%純粋な関数型を実現するのは非現実的であり、大体85%ぐらいを目指せばいいのではないかと主張する。そして、残りの15%は注意深く分離してコードの中に分散しないようにすべきであると。

副作用がある部分を切り離すデザインは、Phoenix Frameworkで採用されているデータアクセスのフレームワーク、Ectoの中にも見られた。Ectoでは副作用のない Model と、副作用を担当する Repository を分けるという、いわゆる Repositoryパターンが採用されている。Railsの Active Record が Model 自身に副作用を内包するのとは対照的である。

Elixir試飲録 (5) – Elixir/Phoenixのホットデプロイ完全自動化(2016年1月版)