Rubyを使って「なぜ関数プログラミングは重要か」を解読しよう!(その3)

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

Rubyを使って「なぜ関数プログラミングは重要か」を解読しよう!(その3) : melborne.github.com

                                                                                                            • -

引き続き「なぜ関数プログラミングは重要か」を
Rubyを使って解釈し自分の理解に基づいて解説してみます
誤解が有るかも知れません
いやきっとあります
ご指摘いただければ助かります

プログラムの貼り合せ(遅延評価)

次に関数プログラミングの2つ目の強力な糊
つまりプログラムを貼り合せる糊について説明する


いま2つのプログラムfとgがあって
入力inputをこれらに適用する場合を考える

g (f input)


プログラムfは入力inputを受け取ってその出力を計算し
その出力はプログラムgの入力として使われる


一般的なプログラム言語ではfからの出力を
一時的にメモリーに蓄えることでその実装を可能とするが
ケースによってはメモリー占有量が膨大になり得る


関数プログラミングではプログラムfとgは厳密な同期の上で走る
つまりプログラムfはプログラムgが必要とする分だけ
実行されて残りは破棄される
このことからプログラムfは
無限に出力を生成し続けるものであってもよい
これによってプログラムの停止条件
ループ本体と切り離すことができ
強力なモジュール化が可能になる


このようなプログラムの評価方式は「遅延評価」と呼ばれる

ニュートンーラプソン法による平方根

遅延評価の力を使って
ニュートンーラプソン法による平方根アルゴリズムを求めてみよう
この方法でnの平方根を求めるとき任意の近似値xを選び
xとn/xの平均を取っていくことでより良い近似値xを得る
これを繰り返し十分に良い近似値が得られたら処理を終える
良い近似値かの判断は隣接する近似値の差が
許容誤差eps以下であるかにより判断する


Rubyにおける一般的な実装は以下のようになるだろう

 EPS = 0.0001    # 許容誤差
 A0 = 1.0        # 初期近似値

 def sqrt(n, x=A0, eps = EPS)
   loop do
     y = x
     x = (x + n/x) / 2.0           # 次の近似値
     return x if (x-y).abs < eps
   end
 end

 sqrt 2 # => 1.4142135623746899
 sqrt 5 # => 2.236067977499978
 sqrt 8 # => 2.8284271250498643


この実装ではループの停止条件
ループに組み込まれてしまって分離できない
遅延評価を使うことによって実装のモジュール化を行い
その部品が他の場面でも使えることを示そう


基本的にRubyの関数(メソッド)は正格評価であり遅延評価されない
しかし関数をProcやEnumeratorオブジェクトとすることによって
その評価のタイミングを遅らせる
つまり遅延評価させることができる


まず次の近似値を計算するnext_valを定義しよう

 def next_val
   ->n,x{ (x + n/x) / 2.0 }.curry
 end


next_val*1
求める平方根の数値nと近似値xを取って次の近似値を返すが
これをcurry化されたProcオブジェクトを返すように実装する
これによって
2つの引数を渡すタイミングをコントロールできるようになる
つまり数値nだけを先に渡すことによってnext_valは
1つの引数xを受ける関数に変わる


例を示そう

 next_for_five = next_val[5]
 nx = next_for_five[1.0] # => 3.0
 nx = next_for_five[nx] # => 2.3333333333333335
 nx = next_for_five[nx] # => 2.238095238095238
 nx = next_for_five[nx] # => 2.2360688956433634


次に初期値に任意の関数を繰り返し適用して
その結果のリストを返す汎用関数repeatを定義しよう

 def repeat(f, x)
   Enumerator.new { |y| loop { y << x; x = f[x] } }
 end


repeat関数は1つの引数を取って1つの結果を返す関数fと
fの初期値となるxを取りEnumeratorオブジェクトを返す
Enumeratorのブロックの中では
loopによってxを関数fに適用した結果が
繰り返しyつまりEnumerator::Yielderオブジェクトに渡されるが
これはEnumeratorオブジェクトが呼び出されるまで実行されず
そのため無限ループにはならない


このrepeat関数に先のnext_val関数を渡すことによって
平方根nの近似値のリストが得られる

 approxs = repeat next_val[5], 1.0 # => #<Enumerator: #<Enumerator::Generator:0x0a4aec>:each>

 ls = []
 5.times { ls << approxs.next }
 ls # => [1.0, 3.0, 2.3333333333333335, 2.238095238095238, 2.2360688956433634]


Enumeratorオブジェクトはその呼び出し*2の度に
ループを1つ回して結果を1つ返す
repeatはその出力を利用する関数と同期して
それが必要とされる分だけ評価される
つまりrepeatそれ自体は繰り返し回数の制限を持たない


次に関数with_inを定義しよう
with_inは許容誤差と近似値のリスト*3を引数に取り
許容誤差よりも小さい2つの連続する近似値を探す

 def with_in(eps, enum)
   a, b = enum.next, enum.peek
   return b if (a-b).abs <= eps
   with_in(eps, enum)
 end


最初の行でEnumeratorオブジェクトの返す
最初の2つの値をnextとpeekでa, bに取る
Enumerator#peekはカーソルを進めないで先頭要素を取る
2行目の終了条件が満たされない限り
処理は再帰的に繰り返えされる


最後にこれらの部品を使って
平方根を求める関数sqrtを定義しよう

 EPS = 0.0001    # 許容誤差
 A0 = 1.0        # 初期近似値

 def sqrt(n, a0=A0, eps=EPS)
   with_in eps, repeat(next_val[n], a0)
 end

 sqrt 2 # => 1.4142135623746899
 sqrt 3 # => 1.7320508100147274
 sqrt 5 # => 2.236067977499978
 sqrt 8 # => 2.8284271250498643


sqrt関数はこのようにしてモジュール化された3つの汎用部品
next_val repeat with_inを貼り合せて作ることができた


sqrt関数はモジュールを合成して構成されているので
プログラムの基本的な構造を変えることなく変更が容易に行える


今度は
2つの連続する近似値の差がゼロに近づくという条件の代わりに
2つの近似値の比が1に近づくという条件に変えてみよう
これは非常に小さいまたは非常に大きい数に対しては
より適切な結果を出す


この目的を達成するには
関数with_inに代わる関数relativeを定義するだけでよい

 def relative(eps, enum)
   a, b = enum.next, enum.peek
   return b if (a-b).abs <= eps*b.abs
   relative(eps, enum)
 end

 def sqrt(n, a0=A0, eps=EPS)
   relative eps, repeat(next_approx[n], a0)
 end

 sqrt 2 # => 1.4142135623746899
 sqrt 3 # => 1.7320508100147274
 sqrt 5 # => 2.236067977499978
 sqrt 8 # => 2.8284271250498643


他の部品を変えることなく新しいsqrt関数ができた


(続く?)

*1:nextはRuby予約語なので使えない

*2:ここではnext

*3:正確にはリストではなくEnumeratorオブジェクト