<< 1 2 3 >> |
PythonでCleanやHaskellのリスト相当を準備で、関数プログラミングの準備ができたので、いよいよ関数プログラミングにチャレンジしてみよう。その前に、「関数プログラミングって何がどういいの?」という人は、 なぜ関数プログラミングは重要かを一読しておこう。 まず、Consリストを生成するいくつかの関数を定義しよう。手始めに、指定された値の無限長のConsリストを生成する関数repeat()を作る。
次に等差数列を生成する関数を定義する。
Pythonのzip関数に類似した、二つのConsリストを、二つの要素のタプルの単一のConsリストに変換するzipCons関数を定義する。
次に、Consリストを操作する基本的な関数foldrを定義しよう。なぜ関数プログラミングは重要かでは、reduceという名で定義されている関数だ。foldrの実行例を示す。
上記の呼び出しは、次のように評価される。
上記の式を見てわかるように、リストが右(right)から畳み込まれる(fold)。だからCleanでは、関数名はfoldrと呼ばれる。 関数foldrの実装は次のようになる。
foldrは、Consリストを右から畳み込むのでConsリストが長すぎるとスタックオーバーフローを起こす。これは、純粋関数言語のCleanでも同様のことが起きる。Cleanで下記のように無限に続く整数リストを足していくと("[1..]"は1,2,…という無限に続くリストを表す)、"Stack overflow."となる。
同じようにConsリストを畳み込む関数として、関数foldlを考えてみよう。名前から想像できるようにこの関数はConsリストを左から畳み込む。
上記の呼び出しは、次のように評価される。
foldlは、Consリストの先頭から処理を行うため、無限に続くConsリストに対して(処理を完了することはできないが)処理を開始することができる。関数foldlを実装してみよう。
このfoldlの実装は、foldr同様再帰呼び出しを行っているため、長いリストにfoldlを適用するとスタックオーバーフローとなる。これはかなり困ったことだ。幸い、左から畳み込む場合、再帰を使わなくても計算を行うことができる。上記の下線部に注目しよう。下線部の計算が済めば、このConsノードに関する文脈を保存する必要はない。一方foldrの場合は、右側のConsノードに対する計算が終わった後現在のノードに関する計算を行うため、現在のノードを処理する文脈を保存する必要がある。foldlは行きがけに計算する、foldrは帰りがけに計算する関数といえるだろう。行きがけに計算する再帰は末尾再帰(再帰呼び出しが末尾に来るから)と呼ばれる。C言語とかでも条件が整えばgccは末尾再帰をループに展開する最適化を行い、スタックを消費しないコードを出力する。末尾再帰はループで書き直すことができるということだ。foldlをループを使って再実装してみよう。
以上で、Consリストを操作する基本的な関数群が出来上がった。早速これらを使っていくつか関数を作ってみよう。まず、Consリストを読みやすい文字列に変換するstrCons関数を作ろう。
最後に、Consリストの先頭のnノードからなるConsリストを返す関数take()を作る。これまで、無限に続くConsリストを扱ってきたので、この関数がないといつまでたっても処理が終わらないからね。 これで、やっとこれまで作ってきたConsリストを操作する関数の動作を確認することができる。
いくつか動作確認してみよう。
このように、Pythonでもかなり関数型言語的な記述ができることがわかる。今のところどの関数もシンプルに実装できている。しかし、関数プログラミングに慣れていない頭には、シンプルだけど難しく感じられる。上記のコードを書いていると、普段使っていない脳細胞を使っている感じがする。 |