[入門]関数プログラミング―質の高いコードをすばやく直感的に書ける!

第2章 関数プログラミングのパラダイム―命令プログラミングと何が違うのか

この記事を読むのに必要な時間:およそ 5.5 分

reduceを使う

次にreduceを使います。reduceとは,リストの要素を何らかの方法で1つの値に集約することです。関数プログラミングでは,これを「畳み込み」と呼びます。畳み込みの関数としてreduceが使われるのはCommon Lispで,ほかの言語ではfoldと呼ばれることが多いようです。

今やりたいことは,整数のリストの要素すべてを足し合わせることです。これを畳み込みで実現するには,次のような計算をすることになります。

((((0 + 0) + 20) + 60) + 120) + 200

つまり,初期値0に対して,要素を順に足していくのです。Haskellの畳み込み関数foldlは,第一引数に演算子,第二引数に初期値,第三引数にリストを取ります。foldlも高階関数です。Haskellの文法では,演算子を引数に指定する場合,次のように丸括弧で囲む必要があるので注意してください。

> foldl (+) 0 ret2
400

答えが出てきました!

プログラムにまとめる

では,以上の結果をまとめて関数calcを定義してみましょう。

> let mul (i,x) = x * i
> let calc xs = foldl (+) 0 (map mul (zip [0..] xs))

Haskellでは,リストを表す変数名を複数形にする習慣があります。上記では,変数xsがその例になります。リスト [10,20,30,40,50] を与えて実行してみます。

> calc [10,20,30,40,50]
400

再代入なしに問題が解けました!

このように関数型言語では,式と式とをつないでより大きな式を作っていきます。筆者が関数プログラミングを勉強したときに難しいと感じたのは,プログラム全体を1つの式で表現するという制約でした。文を列挙することに慣れていた頭には,大きな発想の転換が必要だったのです。

ファイルに書き出す

これまで対話環境でプログラムを作成してきましたが,これをファイルに書き出しましょう。Haskellプログラムの拡張子は「.hs」です。今回は,次のプログラムをcalc.hsとして保存します。

mul (i,x) = x * i
calc xs = foldl (+) 0 (map mul (zip [0..] xs))

対話環境ではないので,letを削る必要があります。このファイルを対話環境に読み込ませるには,コマンドghciの引数としてファイルcalc.hsを指定します。

% ghci calc.hs
Main>

ファイルを読み込むとプロンプトがPrelude>から変化します。以降はこちらも>と略記します。

calcを使ってみましょう。

> calc [10,20,30,40,50]
400

calc.hsから読み込んだ,関数calcを評価できました。

繰り返しと再帰との関係

再代入なしでも,値を新しく作り出せば問題が解けることがわかったと思います。ただ,まだすっきりしていないところがあるかもしれません。for文がないのに,繰り返しはどうやって実現したのでしょうか?

リストを走査するという繰り返しの機能は,zip,map,そしてfoldl自体が備えていて,それは再帰で実現されています。次にmapの実装を示します。

map f [] = []
-- mapがmap自身を呼び出している
map f (x:xs) = f x : map f xs

このプログラムの意味を理解する必要はありません。mapがmap自身を呼び出しているのがわかれば十分です。このように関数プログラミングでは,繰り返しを再帰で実現します。ただ,再帰を使うよりも,適切な高階関数を利用するほうが良いプログラミングスタイルだとされています。

関数プログラミングの特徴:部品プログラミング

関数型言語では,関数に一部の引数を与えることで,カスタマイズした関数を作り出す機能があります。これは「部分適用」と呼ばれています。

例として,mapとmulを取り上げましょう。mapは汎用的な関数ですが,これにmulを与えて,mulをmapする専用の関数mapMulを作り出したいとします。calc.hsを対話環境に読み込ませた状態で,次のように入力してください。

> let mapMul = map mul

mapは二引数の関数でしたが,引数1つは埋まったので,mapMulは一引数の関数です。mapMulを使ってみましょう。

> mapMul [(0,10),(1,20),(2,30)]
[0,20,60]

また,Haskell特有の話になりますが,関数合成の演算子「.」が用意されています。これは,一引数の関数を連結していける演算子です。calcの実装を関数の連鎖に変えてみましょう。高階関数に部分適用することで専用の一引数関数を作り,それを「.」で連結するのです。次のようになります。

let calc = foldl (+) 0 . map mul . zip [0..]

このプログラムの意味が正確にわからなくてもかまいません。ただ,図1のように,小さな部品から大きな部品が組み立てられていることがわかれば十分です。図1からわかるように,関数型ではデータが流れる信号処理回路のようなイメージでプログラムを書いていきます。

図1 部品としての関数

図1 部品としての関数

関数プログラミングでは,関数自体が部品だと言われる所以(ゆえん)をまとめます。

  • 汎用の部品である高階関数を部分適用でカスタマイズして新たな部品を作る
  • それらをつなぎ合わせてさらに大きな部品を作る

ここで,JavaScriptで実現したcalcをもう一度見てください。仕事をやり過ぎているように思えてきませんか?

著者プロフィール

山本和彦(やまもとかずひこ)

株式会社IIJイノベーションインスティテュート(IIJ-II)技術研究所 主幹研究員。

開発した代表的なオープンソフトにMew,Firemacs,Mightyがある。『プログラミングHaskell』や『Haskellによる並列・並行プログラミング』の翻訳者。職場ではHaskell,家庭では3人の子供と格闘する日々を送っている。

web:http://www.mew.org/~kazu/

twitter:@kazu_yamamoto