「関数型プログラミングって何?」日本語訳
この記事は、技術翻訳 Advent Calendar 2016 の15日目です(枠が空いてたので勝手にお邪魔してます)。前回(6日目)は、id:msyksphinz さんの「個人が趣味で技術書を翻訳するという意義について」でした。
今回ご紹介するのは、昨年末に公開された Kris Jenkins さん (@krisajenkins) の "What Is Functional Programming?" です。日本語訳の公開については著者から承諾済みです。また、London Functional Programmers meetup での同タイトルの講演動画が公開されています。
関数型プログラミングの考え方は、世間ではどうも小難しい話だと思われている節があります。その理由の一つに、議論の抽象度が(比較的)高いことが挙げられるでしょう。例えば、以前このブログで紹介した「なぜ関数プログラミングは重要か」も、関数型プログラミングの本質をコンパクトに抽出した優れた論文なのですが、いかんせん数学的な考え方に慣れていない方にはとっつきにくい面がありました。
しかし、この記事では、我々が日々の仕事をこなしていく上で、関数型プログラミングがいかに実践的な方法論であるかを平易かつ具体的に説いています。特に、数学は一切出てこないのでご安心ください。
(いちおう技術翻訳ネタにも触れておくと、この記事、読む分にはスルスル読めたので楽勝かなーと思ってたんだけどそんなことは無かった…。以前 ScalaMatsuri ブログでも検証した通り、Google 翻訳の精度が上がってるので併用したら少しは楽になるかと思ったけど、こういう口語的な言い回しを多用する文章にはまるで無力でした。まる。)
関数型プログラミングって何? (What Is Functional Programming?)
これは、関数型プログラミングの本質は何なのかってことについての僕の見解で、とにかく目先の仕事を片づけたいと思っている職業プログラマ (jobbing programmer) に向けて書いたものだ。
この記事で僕が伝えたいのは、君が書くあらゆる関数には二組の入力と二組の出力があるってことだ。
二つ? え、一つだけでしょ?
いいや、二つ。間違いなく二つだ。一つ目のペアについて例を見てみよう:
public int square(int x) { return x * x; } // 補足: これが何の言語かは重要じゃないけど、入力と出力の型を強調するために明示的に宣言する言語を選んだ。
これを見た君は、この関数の入力は int x
で出力は int
だと考えるんじゃないか。
それが入力と出力の一組目で、言うなれば従来の捉え方だ。では続いて、入力と出力の二組目の例を見てみよう:
public void processNext() { Message message = InboxQueue.popMessage(); if (message != null) { process(message); } }
この関数は、構文を見るかぎりは何も入力を取らず何も出力を返さないように見えるが、何かに対して明らかに依存しているし、明らかに何かをしている。実は、この関数には入力と出力の組が隠れている。その隠れた入力とは popMessage()
を呼び出す前の InboxQueue
の状態だ。そして、隠れた出力とは process
が引き起こしたあらゆる結果と、それに加えて、処理が終わった後の InboxQueue
の状態だ。
間違いなく、InboxQueue
の状態はこの関数の本物の入力だ。その値を知らなければ processNext
の挙動も分からない。そして、出力の方も本物だ。processNext
を呼び出した結果は、InboxQueue
の新たな状態を考慮しないと完全に理解することはできない。
このように、二番目のコード片には入力と出力が隠れている。それは何かを必要として、そして何かを引き起こすが、API を見ただけではそれが何なのかは決して推測できない。
この隠れた入出力にはちゃんとした名前があって、その名を「副作用」という。さまざまな種類の副作用があるけど、それらは全て同じコンセプトの下でまとめられる。「引数リストに含まれないけど、この関数の呼び出しに必要なものは? そして、戻り値に含まれないけど行うことは?」
(僕は実のところ、隠れた出力を表す「副作用 (side-effect)」という語だけでなく、隠れた入力を表す「副原因 (side-cause)」という語も必要だと思う。これ以降の記事では、ほとんどの場所では簡潔に「副作用」とだけ書くが、その場合は間違いなく副原因の話もしている。僕は、あらゆる隠れた入出力についての話をしている。)
副作用は複雑性の氷山だ
関数が副作用(と副原因)を持つとき、こう見立てることができる:
public boolean processMessage(Channel channel) {...}
…で、これを見た君は、これが何をしているのか理解したと思うかもしれないが、それは全くの誤りだ。関数の中身を見なければ、その関数に何が必要なのか、あるいはその関数が何を行うのかを知る方法はない。チャンネルからメッセージを取り出して処理する? たぶん。何かの条件を満たしたらチャンネルを閉じる? おそらく。どこかのデータベースのカウンタを更新するのかな? ひょっとすると。期待したログディレクトリのパスを見つけられなかったら爆発するの? かもしれない。
副作用は複雑性の氷山だ。関数のシグネチャを、そして名前を見たとき、君はそれが何物なのかを何となく分かった気になる。しかし、関数シグネチャの表層の下には、本当にあらゆるものが隠れている可能性がある。あらゆる要件、あらゆる変更、そうしたものが隠れている。実際に何が関わっているかは、関数の実装を見なければ知る由もない。API の表層の下には、さらなる複雑性の膨大な塊があるかもしれない。それを把握する方法は三つしかない: 関数の定義に飛び込むか、複雑性を表層に持ってくるか、無視を決め込んでうまくいくことを祈るか。そして、無視すると、たいてい結局はタイタニック号と同じ過ちを犯すことになる。
これってカプセル化の話じゃないの?
違うよ。
カプセル化とは、実装の詳細を隠蔽することだ。呼び出し元がコードの内部のことを心配しなくても済むように隠そうって話だ。これは変わらず良い方針だけど、この記事で話してることじゃない。
副作用に注目するのは「実装の詳細を隠蔽したい」からではなく、コードとその外側の世界との関係を隠蔽したいからだ。副原因を伴う関数には、その関数が外部のどんな要素に依存しているかについて文書化されていない仮定がある。副作用を伴う関数には、その関数が外部のどんな要素を変化させるかについて文書化されていない仮定がある。
副作用は悪なのか?
いや、それがプログラムの元々の作者が期待した通りに動作するならおそらく大丈夫だ。ただ、副作用の難しいところは、僕らは元のプログラマが暗黙に期待していたことが正しいと、そしていくら時間が経っても変わることなく正しいと信じる必要があるってことだ。
僕らは、この関数を書いた際に期待していた通りに世界の状態を設定しただろうか? さもなくば、世界がどこかで変更された? それはおそらく、一見して繋がりのないコード片が変更されたせいだ。そうでなければ、そのソフトウェアを新しい環境にインストールしたからだ。世界の状態について隠された仮定があるとき、僕らは、世界の状態と、その関数が十分に動作する状態とが似ていると暗に期待していることになる。
このコードはテストできるだろうか? 単独では無理だ。回路基板と違って、僕らは入力にそのままプラグを差し込んだり出力を確認したりできない。僕らはコードをこじ開け、その隠れた原因や作用を把握し、それが存在するはずの世界をシミュレートする必要がある。僕は、テスト駆動開発に取り組んでいる人々が、テストをブラックボックスでやるべきかホワイトボックスでやるべきかについて堂々巡りしているのを見てきた。答えはブラックボックスでやるべきで、実装の詳細を無視できるはずだけど、副作用を許してしまえばそれはできない。副作用はブラックボックステストへの扉を閉ざしてしまう。なぜなら、箱をこじ開けて中身を調べなければ入出力へ到達できなくなるからだ。
これは、デバッグにおいてさらに問題になる。関数が副作用(や副原因)を許さなければ、その関数が正しいかどうか理解するには、単にいくつか入力を与えて出力を確認するだけでいい。だが、副作用を伴う関数だったら? システムの他の部分に対する影響を際限なく考慮しなきゃいけなくなる。関数が何かに対して依存したり影響を引き起こしたりするのを許容すると、あらゆる場所にバグが存在するようになる。
副作用は常に表層に移せる
僕らは、この複雑性に対して何かできるだろうか? うん、始めるのは実のところかなり簡単だ: 関数が何か入力を持つなら、そう言おう。何かを出力として返すなら、そう宣言しよう。それだけだ。
例を試してみよう。この関数には入力が隠れている。素早く見つけられたらボーナスポイントだ:
/** * 訳注: 指定したチャンネルで現在放映しているテレビ番組を返す関数。 */ public Program getCurrentProgram(TVGuide guide, int channel) { Schedule schedule = guide.getSchedule(channel); Program current = schedule.programAt(new Date()); return current; }
この関数には現在時刻 (new Date()
) という入力が隠れている。この複雑性を表層に移すには、こういう追加の入力があるってことをシグネチャの中で正直に表すだけでいい:
/** * 訳注: 第三引数に when を追加して programAt(Date) に与えている。 */ public Program getProgramAt(TVGuide guide, int channel, Date when) { Schedule schedule = guide.getSchedule(channel); Program program = schedule.programAt(when); return program; }
これで、この関数は入力(や出力)を隠し持たなくなった。
この新バージョンの良い点と悪い点を見てみよう:
悪い点
より複雑になったように見える。関数の引数が二つから三つに増えたし。
良い点
複雑にはなっていない。依存関係を隠してもシンプルにはならないし、それを正直に表したらより複雑になるってこともない。
テストははるかに簡単になっている。一日の異なる時刻、時計の変化、うるう年といったすべてのテストがそのまま書ける。なぜなら、好きな時刻を渡せるからだ。僕は、最初のバージョンのような本番コードをテストするために、あらゆる種類の巧妙なトリックで現在のシステムクロックを偽装するのを見てきた。これをパラメータ化するだけで、そんな苦労をしなくて済むんだ!
推論もより簡単になっている。いまや、関数は単に入力と出力の関係を記述しているだけだ。入力が分かれば出力がどうなっているべきか分かるし、そして結果についてあらゆることが分かる。これはすごいことだ。このコードは単独で検証できる。入力と出力の関係さえテストすれば関数全体をテストしたことになる。
(そして余談だけど、便利なのはこれだけじゃない。「一時間後に始まる番組」を返すようなコードがタダで手に入る。)
「純粋関数」って何?
ドラムロールをどうぞ。
これで隠れた入力と出力について把握したので、ついに「職業プログラマ向けの純粋関数の定義」を説明できる:
関数が〈純粋〉であるとは、全ての入力が入力として包み隠さず宣言されていて、同様に出力が出力として宣言されていることだ。
逆に、入力や出力を隠し持っている関数は〈純粋〉ではない。非純粋な関数にとっては、僕らが「関数が提供する契約」だと思っているものは全体のうちの半分に過ぎない。複雑性の氷山が迫っている。純粋でないコードは決して「単独」では使えないし、単独でテストすることもできない。テストやデバッグをしたいときはいつも、そのコードが依存する他のコードを追跡しなければならなくなる。
「関数型プログラミング」って何?
純粋関数と非純粋関数について把握したので、これで「職業プログラマ向けの関数型プログラミングの定義」を説明できる:
関数型プログラミングとは、純粋な関数を書いて隠れた入出力をなるべく取り除き、できるだけ多くのコードを入力と出力の関係だけで記述することだ。
ほとんどのプログラムは何かを返すというより何かを行うために実行されるので、どうしたっていくらかの副作用は避けられないけど、プログラムの中は厳格に制御しよう。副作用(と副原因)を可能な限り排除して、それでも取り除けない場合は厳重に管理するんだ。
別の言い方をするなら、「コード片に必要なものや、コード片が生成する結果を隠蔽するのはやめよう」ってことだ。コード片を正しく実行するのに必要なものがあるなら、そう言おう。コード片が何か有益なことをするなら、それを出力として宣言しよう。そうすれば、僕らのコードはよりクリーンになる。複雑性を表層に移せば、それをかみ砕いて対処することができる。
「関数型プログラミング言語」って何?
あらゆる言語は純粋関数をサポートしている ― 例えば add(x, y)
を非純粋関数にするのは難しい。*1そして多くの場合、非純粋関数を純粋関数に変換するには、関数の入力と出力をすべてシグネチャに持ち上げるだけでいい。そうすれば、シグネチャは関数の挙動を完全に記述するようになる。じゃあ、あらゆるプログラミング言語は「関数型」なのか?
そんなことはない。もしそうなら、「関数型プログラミング言語」なんて言葉は無意味だってことになる。
それじゃあ、「職業プログラマ向けの関数型プログラミング言語の定義」はなんて説明すればいいんだろう?
関数型プログラミング言語とは、副作用なしでプログラミングすることをサポートしたり奨励したりするような言語だ。
より具体的には、関数型言語は副作用の積極的な排除を可能な限り助け、それができない場合は厳重に制御するのに役立つ。
より過激な言い方をするなら、関数型言語は副作用に対して積極的に敵対する。副作用は複雑で、複雑さはバグで、バグは悪魔だ。関数型言語は君が副作用に敵対することも助け、君と共に副作用を力で屈服させるだろう。
それで全部?
そうだ。きっとこれまで、君はそれが隠れた入力であると考えたことがないような微妙なものもいくつかあるだろうけど、それが本質だ。「副作用が第一の敵である」という観点でソフトウェアを構築し始めると、君がプログラミングについて知っているあらゆることが変化するだろう。この記事のパート 2 では、副作用と関数型プログラミングについて把握した上で、プログラミングの現状に散弾銃をブッ放すことにしたい。
謝辞
この記事は、関数型プログラミングの性質について何回か議論したことが元になっている。特に Sleepyfox との「適切なライブラリを組み合わせれば JavaScript は関数型プログラミング言語とみなせるか」を巡る議論からは大いに刺激を受けた。僕の答えは直感的にはノーだけど、それがなぜかを考えることを通じて、非常に実りの多い思索に誘われるきっかけになった。
James Henderson に敬意を。今年は、彼のおかげで関数型について多くの実りあるアイデアに触れることができた。
そして、Malcolm Sparks, Ivan Uemlianin、Joel Clermont、Katy Moe、そして僕のドッペルゲンガー Chris Jenkins。皆の校正と提言に感謝する。
おまけ: Part 2 の概要
パート 2 を翻訳する元気が残ってないので概要だけ紹介しておきます。
関数型プログラミングは何ではないか
関数型言語は副作用(副原因)と戦うための道具であって、つまり:
関数型言語はどれ?
設計の臭い
- 引数なしは副原因のシグナル
public Int foo() {}
- 戻り値なしは副作用のシグナル
public void foo(...) {...}