この章では,命令プログラミングと関数プログラミングのパラダイムの違いを理解するために,簡単な計算問題を取り上げます。
命令プログラミングと関数プログラミング
当たり前過ぎて意識されていないかもしれませんが,改めて命令プログラミングのパラダイム(以下「命令型」と略記)を説明すると,次のようになります。
- 命令を列挙する(典型的には命令である文をセミコロンで区切って並べる)
- 状態がある(状態とは再代入可能な変数のこと)
- 再代入を使って状態を変化させる
一方,関数プログラミングのパラダイム(以下「関数型」と略記)は次のようになります。
- 関数を引数に適用する
- 状態はない
- (値を破壊したくなったら)新たな値を作る
状態がないので,変数の値は変わりません。これが関数プログラミングを永続データプログラミングと定義した理由です。しかし,関数型で本当に問題が解けるのか疑問だと思います。これから簡単な計算問題を使って,両パラダイムの違いを浮き彫りにします。
パラダイムの違いがわかる例題
次のような問題を解く関数calcを定義することを考えます。
- 入力として整数の配列あるいはリストが与えられる(たとえば[10,20,30,40,50])
- 0から数えてn番目の要素にはnを掛ける
- それらすべてを足し合わせて出力として返す
つまり,次のような計算をします。
10 * 0 + 20 * 1 + 30 * 2 + 40 * 3 + 50 * 4
簡単ですね。
命令的なfor文による問題解決
命令プログラマであれば,この問題をfor文か類似のループで解くでしょう。実際にJavaScriptで関数calcを書いてみます。for文を使うと,次のように実装できます。
calc.js
function calc(ar) {
var ret = 0;
for (var i=0; i < ar.length; i++) {
ret = ret + ar[i]*i;
}
return ret;
}
このcalcは,命令型の特徴を備えています。
- セミコロンで区切って命令を列挙している
- 状態retとiがある
- retとiはループが回るたびに再代入されている
実際に動かしてみましょう。次のコードはRhinoを使い,入力として配列[10,20,30,40,50]を与える例です。
% rhino
js> load("calc.js")
js> calc([10,20,30,40,50]);
400
正しい答え400が出てきました。
命令プログラマであれば,これとはまったく異なる解決方法があるとは想像もできないかもしれません。また,このプログラムは十分に簡潔で,これ以上分割はできないと思うでしょう。しかし,関数プログラマから見ると,この関数は仕事のやり過ぎに見えます。
少し話がそれますが,筆者がJavaScriptを勉強していたころ,JavaScriptの神様であるDouglas Crockford氏のエッセイをよく読んでいました。エッセイの一つに「JavaScript:世界で最も誤解されたプログラミング言語」があります。このエッセイで筆者が衝撃を受けたのは,「不格好なforステートメント」という言葉でした。for文とは不格好なのでしょうか? この疑問は,関数プログラミングを習得することでようやく氷解しました。
関数的なMapReduceによる問題解決
関数プログラミングでは,MapReduceという方法で解きます。MapReduceと言うと,Googleの分散処理フレームワークが有名ですが,このシステムのアイデアの根源は関数型のmapとreduceを使う手法です。さっそく,対話環境でMapReduceを試してみましょう。
リストをまとめる
Haskellでは,リストのリテラルが用意されており,角括弧の中に要素をカンマで区切って書きます。同じ表記がJavaScriptでは配列でしたが,Haskellではリストを意味します。整数のリストのリテラルを評価してみましょう(プロンプトPrelude>
は>
と略記します)。
> [10,20,30,40,50]
[10,20,30,40,50]
整数のリストそのものが返ってきました。次に,このリストの要素と掛け合わせるカウンタを用意しなくてはいけません。Haskellでは,[はじめ..おわり]
と書くと,値を増やしながら列挙してくれます。
> [0..4]
[0,1,2,3,4]
リストが2つあると扱いづらいので,常套手段である関数zipで1つにまとめましょう。
Haskellでは,関数を呼び出す際に,関数のあとの引数を丸括弧で囲む必要はありません。引数はカンマではなく,空白で区切って列挙します。
> zip [0..] [10,20,30,40,50]
[(0,10),(1,20),(2,30),(3,40),(4,50)]
リストが1つ新たに生成されました。この要素は(0,10)のように,カウンタと整数の組になっています。このように表記される組は,無名の構造体であると理解するとよいでしょう。
zipの第一引数であるリストには「おわり」が指定されていません。この場合,必要なだけ生成することを意味します。zipは,短いほうのリストに合わせて組のリストを生成しますので,実際に第一引数は第二引数の長さ分だけカウンタを生成します。
あとで利用しやすいように,上記の結果にret1という名前を付けておきましょう。対話環境では,変数を定義する場合にletを使います。
> let ret1 = zip [0..] [10,20,30,40,50]
念のため,ret1自体を評価することで,ret1の中身を調べてみましょう。
> ret1
[(0,10),(1,20),(2,30),(3,40),(4,50)]
mapを使う
いよいよmapを使ってみましょう。mapとは,第一引数に関数を,第二引数にリストを取ります。そして,その関数をリストの要素それぞれに適用します。関数を引数に取る関数は,高階関数と呼ばれるのでした。mapは,最も基本的で最も有名な高階関数です。
今やりたいことは,カウンタと整数を掛け合わせることです。そこでまず,カウンタと整数の組を取り,互いを掛け合わせる関数mulを定義してみましょう。対話環境では,関数を定義する場合もletを使います(注1)。
let mul (i,x) = x * i
この定義には,第3章で説明するパターンマッチが使われています。今は単にこのように書くと思って進んでください。
そして,mapを使ってmulをret1の各要素に適用してみます。
> map mul ret1
[0,20,60,120,200]
整数のリストが新たに生成されました。だんだん答えに近づいてきましたね。これにもret2という名前を付けておきましょう。
> let ret2 = map mul ret1
- 注1)
- 「(i,x)」「x * i」と順番が逆なのは,zipにカウンタ[1..]を部分適用(後述)して簡潔に書くためです。