( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

関数の話

こんにちは、しいたけです。

某所で関数型プログラミングとはリスト処理のことなのか、と燃えているのを見て、関数型プログラミングとは何か、ということを自分なりの考えを述べたいと思いました。春なので。

この資料は2年ほど前にSupershipの社内勉強会で使ったものですが、この中で関数とオブジェクトを対比している箇所があります。 関数もオブジェクトも、変数や関数の引数戻り値として扱える第1級の値であり、状態を持ち(メンバー変数/クロージャ)、組み合わせが可能(delegate, composition/関数合成)、である、と。

ではオブジェクト指向関数型プログラミングで何が決定的に異なるかというと、設計・実装のアプローチに何を中心に据えるか、ということだと思います。

オブジェクト指向では、クラス・オブジェクトをモデリングし、各種のオブジェクト指向デザインパターンを用いてオブジェクト同士を組み合わせながら設計・実装を行います。 関数型プログラミングでは、関数を細かな組み合わせ可能な単位に分解し、関数合成、再帰クロージャなどを駆使しながら組み合わせることで設計・実装します。

こう書いてみると当たり前のことですが、コードの記述スタイルだけ議論しても意味がなくて、そのような記述はプログラム全体を貫く思想のなかでどのような位置づけにあるのかのコンテキストが重要なのではないでしょうか。 例えば、Goでは無理して無名関数を用いたリスト処理を書くより、forで書くほうがはるかに自然ですよね。 JavaScriptでは……どうでしょうか、色々なスタイルで書けるので議論が紛糾するのかも。

map/reduceを使ったリスト処理が関数型プログラミングの全てかというとそうではなく、あくまで関数を中心に据えた考えた方の一つとして、遅延評価 + 高階関数/クロージャの組み合わせによるストリームライクな実装がある、ということだと思います。 関数を中心に据えると、「何をフィルターするか」「要素をどのように変換するか」という処理の単位が関数として抽出され、その組み合わせ方法として遅延評価リストと高階関数を用いる方法がある、というだけです。 結果、関数を中心にすえたアプローチではmap/reduceを使う記述が自然と導かれます。 とはいえ、 リスト処理の実装としては再帰を使う方法もあるので、あくまで関数型プログラミングによるリスト処理の1アプローチにすぎません。

どちらのスタイルで書くのがよいか、というのに絶対的な正解はないと思いますが、関数を中心としたアプローチを知っていると、手続き型のプログラムを書いていても思わぬ場面で役に立つことがあります。 ちょうど最近役にたった実体験のひとつで、 ある rspecのパフォーマンスチューニングの際に遅延評価が役にたったことがあります。

error_msg = generate_error_message(actual_value, expected_value)
expect(actula_value).to have_content(expected_value),  error_msg

こんな感じのrspecのコードで、 assertのエラー時に表示する error_msg を生成する generate_error_message が非常にコストの掛かる処理でした。 通常はassertに失敗する場合の方が稀なので、必要になるまで(assertに失敗するまで) この処理の呼び出しを遅延させることで性能が改善しそうです。

rspecexpect はエラーメッセージの代わりに Proc を渡すことが可能です。 そこで、エラーメッセージを生成する処理をクロージャとして抽出し、 expect に渡すことで遅延評価させることができます。

error_msg_generator = ->() { generate_error_message(actual_value, expected_value) }
expect(actula_value).to have_content(expected_value),  error_msg_generator

関数型プログラミングの知識があると、 expect の引数が Proc を取る、というシグニチャから、遅延評価による性能改善に利用する発想が導かれます。 このように、関数型プログラミングは通常の手続き型プログラミングでもおおいに役立ちますので、双方それぞれの手法を学んでおくとプログラミングの裾野が広がりま。 これが、ぼくが関数型プログラミングを学ぶことをオススメする理由です。

まとめ

しいたけおいしいです。