2015年5月13日水曜日

関数型言語をかじってみた

プラモのパーツが届かないこともあって、GW中の暇つぶしに「関数プログラミング実践入門」という本を買って関数型言語を勉強してみた。

関数型言語とは

いまさら書くまでもないが、関数型言語における関数は数学的な関数と同じで必ず入力により出力が一意に決まるものになっている。

CやJavaのような手続き型の言語における関数では関数内でstaticやglobal変数等アクセスすることが許されている。この結果として同じ入力を与えても出力が異なることが得る。このようにstaticやglobalな変数にアクセスすることを副作用と呼び関数型言語の関数ではこのような副作用を及ぼすことは許されていない。

このような考え方はオブジェクト指向目指す方向とは逆行しているように思える。オブジェクト指向におけるメソッド(関数)は基本的にはそのメソッドが属するオブジェクトのプロパティを参照もしくは変更するためのものなので、その関数から見て外部の状態を参照/変更するという副作用を及ぼすことを目的として作られているといっても過言ではない。

なぜオブジェクト指向がそうなっているかというと、端的に言えばデータの塊(オブジェクト)とそれに対する働きかけ(メソッド)をペアで取り扱うことで、複雑なデータ構造をよりシンプルな表現で操作できることを目指して作られているからと言える。

このため、手続き型言語で作った関数を単純に入出力を全て引数と戻り値で受け渡しするように変えれば関数型のプログラムになるかというと必ずしもそうではない。これだけでは単に引数と戻り値が大量にあって見づらいだけの手続き型プログラムの関数にしかならない。

制御を分割する

関数型言語では前述のような「副作用を及ぼさない」という性質が注目されがちだが、個人的には関数型言語で重要な点は制御の部品化にあると感じた。これについて、読んだ本の例をベースに自分なりの解釈も含めて例を書き直してみる。

お題となっている処理は、頂点座標の移動と回転を組み合わせる処理になる。これをJavascriptで従来通りの手続き型で書くとこのようになる。

for(var i = 0; i < points.length; i++) {
  var p = move(points[i]);
  newPoints[i] = rotate(p);
}

moveとrotateはそれぞれ頂点の移動と回転を行う処理だが、細かい説明をしなくても何の変哲も無いどこでも見かけるようなコードだと思う。

これに対して同じくJavascriptで関数型っぽく書くと以下のようになる。

newPoints = map(combine(rotate, move))(array);

ここで、mapとcombineは以下のように定義されている。

var map = function(f) {
  return function(array) {
    var newArray = new Array(array.length);
    for(var i = i; i < array.length; i++) {
      newArray[i] = f(array[i]);
    }
    return newArray;
  }
}

var combine = function(f, g) {
  return function(x) {
    var tmp = g(x);
    return f(tmp);
  }
}

関数を返す関数なので慣れないと直感的にわかりにくいかもしれないが、単にループと関数の結合を行う処理になっている。

手続き型のプログラムでは「処理Aの結果を処理Bに代入する」、「これを配列の全要素に行う」といった制御をコードとしてべた書きしていたものを、関数型のプログラムではそれぞれを関数(部品)として部品の組み合わせで目的とする制御を表現することが出来るようになっている。

そう言うと手続き型の関数だって部品じゃないのかと思うかもしれないが、手続き型の関数は言ってしまえば制御と処理をごちゃ混ぜにして一塊にしたもの、いわゆるカプセル化であって、関数型のように「ループ」、「処理Aの後に処理Bを行う」といった細かい制御の単位で部品化出来るわけではない。

実際問題上記の手続き型の例を手続き型プログラムの手法で部品化しようとしても、for文全体を一つの関数にするぐらいしかやりようがない。これでは中の処理を変えようとしたら関数の中身を変える必要があるし、for文の部分だけ再利用するなんてことは普通の手続き型プログラムでは出来ない。

部品化のメリット

このように制御を部品化する一つのメリットとしてはコードの再利用性が向上することがあげられる。おそらく手続き型言語のプログラマなら人生で数千回もfor文を書いていただろうが、これがmapという部品として再利用出来るようになる。

しかしながら、もっと大きなメリットとして検証の容易さがある。このように制御を小さくて単純な部品に分割していくことで、個々の部品に対する検証は容易になる。そして、品質が担保された部品を組み合わせていくことで全体として高品質なプログラムを組み上げることが出来る。

こう言うと勘のいい人なら「ちょっとまて」と思うかもしれない。検証や物作りの常識からいって高品質な部品を組み合わせたからといって高品質になるとは限らない。そこで重要になるのが最初に述べた「副作用を及ぼさない」という関数型プログラムの性質になる。

副作用を及ぼす部品を組み合わせるとお互いの副作用が干渉し合って単体では発生しなかった問題が発生する恐れがある。このため、副作用があることを前提とした世界では部品Aと部品Bを組み合わせて新しい部品Cを作ると、部品CはA,B含めた全体としてもう一度検証し直す必要がある。当然、どんどん部品を組み合わせていって大きな部品になってくるとその分検証の規模が増え中身も複雑になる。

一方で、関数型プログラムでは部品(関数)は副作用を及ぼさないため部品Aと部品Bを組み合わせても部品AとBの品質に変化は起きえない。このため、これらを組み合わせて部品Cを作っても、あくまでも部品Cとして新たに作った部分(たとえば部品の組み合わせ方は適切かとか、使う部品は合っているかとか)のみ検証すればよいことになる。

このように関数型プログラムでは、
  • 大きなプログラムを小さな部品に分割する
  • 小さくて単純な部品にすることで検証を容易にする
  • 単純な部品を組み合わせて複雑な処理を実現する
    (副作用を及ぼさないため、組み合わせても品質は落ちない)
という作り方をして初めて本当の意味で関数型プログラムと言えるのだと理解した。

で、どうやって部品を繋げていくのかという話からモナドについて書こうと思ったが、長くなりそうなのでまた別の記事で

0 件のコメント:

コメントを投稿