関数型プログラミングを業務開発で活用するために
HaskellやScala、Erlang/Elixir、Clojureなどの関数型プログラミング言語に興味がある人は多いと思いますが、自分らが日常行なっている業務での開発では到底それらの関数型言語を採用できないのが現実、という場合があるかもしれません。
なので、当面はJavaやGroovy、JS,Ruby,Pythonなどの非関数型プログラミング言語の上で関数型プログラミングスタイルや考え方をなるべく使っていくことでFPの考え方や技法に馴染み広めていき、利点を享受しつつ、将来は大手を振って転職業務開発で本格的な関数型言語を使えるようにしていこうというのが現実的な戦略かもしれません。
以下はそのような目的での洗脳勉強会をやるための架空の資料の目次(案)のようなものです。
「独自研究」の注意
「FPとは何か」の定義について、現時点で一般に合意のある明確な唯一のものはないように見えます。しかし、上の目的を達成するためには「関数型プログラミングの本質は、非関数型言語での適用においても同様に有効だ、OOPと併用できる」という結論になる必要があります。また、良くある説明「関数型プログラミングには定義がなく人によってうんたらかんたら」というのも、「わかんねーよ!!おまえら初心者だと思って馬鹿にしてんのか」という印象を持たれる可能性があります。これらの問題を解決するため、本記事では意図的に独断的にFPの本質的定義を提示します。読者の方の認識と一致しない場合、「一つのあり得る説明のしかた」としてでも見ていただけますとありがたいです。コメント歓迎です。
(追記)
「FPに定義などいらない書けばわかるさ」という意見もあると思いますが、またそれも一面の真理だと思いますが、それだと体験している特性が言語のものなのか、関数型パラダイムのものなのか弁別できません。それでも良いとは思いますが、この記事における独断的定義は、他の言語、特に非関数型プログラミング言語に持ち込んでもなお有効であれば、つまり具体的な関数型言語それぞれの利点を完全に除去しても残っている特性があり利点を発揮し続けるのであれば、それこそが、あるいはそれのみが、個々の言語の利点と言うにはとどまらない関数型プログラミング固有の本質的な有効性と呼ぶに値するのではないか、という仮説に基づいています。ただ、言語個別のメリットを除去することによって「FPとしては残ってるのはほんのちょっぴり」に感じられたらごめんなさい。
関数型プログラミング(FP)とは何か?
- 現象面・表象面では以下のとおり
- (1)再代入をしない
- (2)不変的データを使う(破壊的操作を禁止する)
- (3)値を返さない関数や式・制御構造を使用せず、値や式、返り値を返す関数(メソッド)間の依存関係から構成される式木としてプログラムを記述する
- 以下はFPの必要条件とはしない。つまりFPにおいても許容する。理由は、完全には達成できないし、もしできたとしても入出力をしないプログラムには意味がないため。
- (4)副作用としての入出力禁止。
「FPの本質」とは
- プログラムの実行は、引数を入力として結果を得るような計算であるとみなした上で、
計算結果を得るために必要な値の間の依存関係を、そのまま平たくプログラムに書き出すこと(cf.項書き換え ※)- 結果を得るための計算が、「変更される状態、すなわちプログラムの実行=計算の進行によって刻々と変わっていく値」には依存しないようにする(メモ:図が必要)
- 排除できなければ分離する
- 宣言的とも言う
- ←→命令型、手続き型
- 結果を得るための計算が、「変更される状態、すなわちプログラムの実行=計算の進行によって刻々と変わっていく値」には依存しないようにする(メモ:図が必要)
- この特性から、上の(1)(2)(3)が結果として導入される。また後述の「FPの効用」が得られる。
(追記)
※ここで「項書き換え」をFPの本質と言っているわけではなくて、計算の結果の値を得るために必要な値の集合(葉としての引数・定数、中間計算結果としての節)の依存関係を有向グラフとみたときにプログラムないし着目した関数の評価の過程を通じて、そのグラフが静的に定まっている(ボトムがあるからこの表現が適切かは確信ありませんが)、ということを言っています。項書き換えなりは評価戦略の話であり、結果的には得られる値が同じになるので参考としてリンクを貼りましたが、そこは主眼ではないです。
上の説明に「関数」と言う言葉が出てきませんが?
- それでよい。関数はここでは値の依存グラフを表現するための名前の付いた結節点にすぎない。
(追記)
- 関数の振る舞いの特性に限定した定義にしてしまうと再代入禁止の根拠が薄れる。loopカウンタによる単なるforループの存在や、i=1;i++というコード断片を私はシンプルな統一的根拠によってなるべく否定したい。
- 「関数」の意味するものは言語によって異なる。かといって数学的関数で規定したくもない。だって明確化したいプログラムの振る舞いは、それとは実際に異なるものだから。嘘だと思うなら、純粋関数と信じるものを書き下して、呼び出し前の時刻と呼び出し後の時刻を計測してみると良い。「時間が経過した」という副作用を観測する事ができるから(cf.sleep sort)。電力消費量と熱雑音操作して音楽を鳴らすことだってできるらしいし。
- 計算に必要な値の依存関係に着目するのは、そのような関数概念や副作用概念の定義の差異やブレが介入できないと言う意味で本質的、少なくとも実用的である。
FPでやりたいこと・やれること
- 上記で「禁止」「排除」されたものは誰かが背負うことになる。
- ランタイムがやる←Haskell
- ライブラリがやる←FRP…
- ユーザーコードの中で分離する。
- なので、結局やってることは「副作用の分離と管理」である。
この観点から、前述の「(4)副作用としての入出力」も射程に入る。
入出力が禁止できなくても、分離もしくは管理できれば良し(HaskellのIOアクションがやっていることの一つ)。 - 「FPにおいて(副作用などを)禁止することで得られるものは何か?」
は、問いが間違っていたことになる。
FPは副作用を禁止しない。それを分離・管理する技術である。 - FP以前も副作用、明示的状態管理はなされてきた。FPは、純粋関数、参照透過性という基準を元に明確にコード上に明確に
わかる形で意識してやろうとする、のが違い。
純粋関数と参照透過性
不純さは伝染する
tbd
FPではないもの
- 「狭義のFP」は、前述のとおり(異論はあるであろうが、この記事ではそうする)
- 「広義のFP」は、高階関数があればFPだというタワゴト含めいろいろあるので、
おいとく。 - 「パターンマッチ、遅延評価、タプル、ADT、静的型付け、高階型に代表される強力な型システム、型クラス、定理証明、カリー化と部分適用、モナド、do記法、内包表記、第一級関数、高階関数」などはFPではない。
- ただしいずれもFPの適用を助ける面がある。これらがFPの本質と不可分だ、と言いたくなる気持を持つ人も出てくるだろうし、その気持はわからんでもない。しかし本記事ではそういう主張は趣旨に反するので抑える。
- 上記を提供するのがいわゆる「関数型言語」の存在意義であり、実際有用。
FPの効用
- 品質向上、バグ減少
- 状態更新がないので動作の見通しが立てやすく、コード修正(順序変更、追加、削除)の影響が限定的で安定的。
- プログラミング言語がソフトウェアの品質に与える影響
- 合成可能性の向上、部品化
- 文脈としての状態に依存しないので異なる場所での利用がしやすい。
- 関数合成によるプログラミング(モナド、アロー、コンビネータライブラリ、モナドトランスフォーマー、ミドルウェアパターン、デコレータ関数)★
- テストしやすさの向上
- プロパティテスト★
- 状態を切り出すことによって実現が容易になる機能
- タイムトラベリングデバッグ、…
- 高度な抽象概念をモナド、アロー等で表現・部品化★
演習
(1)ループ操作をコレクションを操作する高階関数で表現する
リスト(言語によっては配列、シーケンスなどで良い)を逆順にするプログラムを関数型プログラミングスタイルで記述せよ。例として、ループで書かれたプログラム(Groovy)を前述のFPの定義に従って修正する。
List func1(List list) {
def a = []
def b = []
def c = []
list.reverseEach {
a.add(it.a)
b.add(it.b)
c.add(it.c)
}
return [a, b, c]
}
println func1([[a:1, b:2, c:3],[a:11, b:12, c:13],[a:21, b:22, c:23]])
// ==> [[21, 11, 1], [22, 12, 2], [23, 13, 3]]
(2)ループの中断を遅延ストリームで表現し、部品化を高次元で達成する
ファイルが複数個(n個)あるとします。それぞれのファイルサイズはわかっています。これらのファイル群のファイルサイズを整数のリスト[s1, s2, s3, .... sn]で表現します(sn>=0)。これをlistと呼びます。listの長さはnになります(n>=0)。この「ファイルサイズのリストlist」が表現するファイルをこの順序で、容量limitSizeのフロッピーディスクにコピーする必要があるとします(limitSize>=0)。合計値がlimitSize以下になるように、ファイルを選択してください。具体的には、listの先頭からの部分リストで、その合計値がlimitSize以下であり、長さが最長のものを返す関数を任意の言語で(関数型っぽく)書いてください。listの順序は変更できませんし、listの先頭からスキップすることなく順にコピーする必要があります。Groovyのコード例(関数型っぽくない元の例)は以下のとおり。
List<Integer> func0(List<Integer> list, int limitSize) {
int sum = 0
for (int i=0; i<list.size(); i++) {
sum += list[i]
if (sum>limitSize) {
if (i == 0) {
return []
}
else {
return list[0..i-1]
}
}
}
return list
}
println func0([15,50,2,20,8,16,7,22], 100) // => [15, 50, 2, 20, 8]
println func0([15], 100) // => [15]
println func0([15,50,2,20,8,16,7,22], 1000) // => [15, 50, 2, 20, 8, 16, 7, 22]
println func0([15,50,2,20,8,16,7,22], 0) // => []
println func0([15,50,2,20,8,16,7,22], 95) // => [15, 50, 2, 20, 8]
println func0([15,50,2,20,8,16,7,22], 94) // => [15, 50, 2, 20]
println func0([], 100) // => []
println func0([15,50,2,0,0,0,0,20,0,0,8,16,7,22,0,0], 94) // => [15, 50, 2, 0, 0, 0, 0, 20, 0, 0]
(3)キューを非破壊的に実装し、状態を切り出す
待ち行列をFP、つまり非破壊的操作のみで実装してみましょう。以下はGroovyの例(破壊的操作での例)です。状態を切り出して、とかになるはずです。並列処理は考えないでも良い例外やエラーについても考慮しても良い(Optionとか)
class Queue<E> {
ArrayList<E> list = []
void add(E e) {
list.add(e)
}
E remove() {
if (list.size() > 0) {
return list.remove(0)
}
else return null
}
}
q = new Queue<Integer>()
q.add(3)
q.add(4)
q.add(5)
assert q.remove() == 3
assert q.remove() == 4
assert q.remove() == 5
assert q.remove() == null
q.add(6)
assert q.remove() == 6
非関数型言語でのFP適用の勘所
- 「(1)再代入をしない」「(2)破壊的操作をしない」
は高階関数の活用により可能。便利な高階関数ライブラリや非破壊的
データ構造のライブラリがあればなおよい(cf.純粋関数型データ構造)。 - 細かく見ると、(2)は以下にわかれる。
- (2-1)状態の処理を持たない
- (2-2)本質的に状態を必要とする処理
- (2-1)は慣れれば容易。練習あるのみ。
- map(collect), fold(join),scan(accumulate),zipなどを息を吐くように使えるようになる。
- 十分な機能をもった高階関数ライブラリは欲しい(Rx,EclipseCollection,underscore,lodash,ES2015...)。
- (2-2)は原理的には「引数に状態を渡して返り値として新しい状態も返す」関数に書きかえれば良い
- ただし読みにくくなる(連続して操作する場合特に)
- 状態書き換え後に、古い状態を使う必要なければむしろ状態をとりちがえるバグの元となる。
- Stateモナド導入できると良いのだが★
- とりあえずの工夫としては「状態の外出しによる純粋操作」と
「状態管理に特化した部分」に分離し、選択的に使用する
(両者の結合はクロージャがあれば簡単)。- もしくは割りきって、状態書き換えを許容する。(特に大きな問題があるわけではない)
OOPとの併用のための留意点
- レシーバ形式のレシーバ(this)をメソッド第一引数として見る
- Java 8のメソッド参照演算子::とGroovyの.&演算子の差異
- 上記のクロージャでの代用
- 参考: open recursion, Haskellオブジェクト指向に触れてみよう〜初級編〜, Lens
- クラス定義、継承、仮想関数
- 例外処理との相性
- 型による失敗の表現: Optional(Option,Maybe,Either)
- 非同期例外(RuntimeException)は副作用
flatMapの真実
tbd
非関数型言語でのFP適用の限界
- 言語のサポートがないので自力でがんばる
- FP言語をたとえばScalaやHaskellで学び、その知識を持ち込む
- 課題は前提知識のない他の開発者には不自然に感じられるかもしれないこと
(本記事でカバーしたい点でもある)。
- 「★」で示したところは、現時点では、非関数型プログラミング言語での開発に無理に
持ち込まない方がよいと思っているところ。 - FPを活用するライブラリの使用が一番問題がない。
- 用途にあえば是非トライを
FPを活用するライブラリなど
- FRP, ReactiveX(Rx), BaconJS
- React/ Redux
- Promise/Future/Observable
- Immutable.JS
- FunctionalJava(fj), Functional Groovy
- Eclipse Collection(旧GS Collection)
関連トピックス紹介
- CQRS
- RESTfulアーキテクチャ
- べき等性
- STM
- Datomic
- イミュータブルデータモデル
- Elm, Frege
さあみなさんも関数型ライフを!