grafi.jp

大学のRubyを使ったプログラミングの入門講義で、友人がsin(x)をテイラー展開して評価するという課題を出されたという。多少手伝いつつ友人が自力でなんとか出来て良かったのだが、どうにもなかなかRubyらしく書けず気持ち悪かった。

特に、テイラー展開の各項の生成過程を、次の項の生成に利用するような書き方をすれば、それが顕著になる。

Note
ここからRuby1.9系しか考慮しない話を始める。大抵の大学のシステムはRuby1.8系だろうから、以降のコードはそのままでは大学の課題には使えない。
Note
でっかい数値入れたらFloat溢れて死んだけど、その辺のチェックもちゃんとするなら必要。
def taylor_sinx_1(x)
  ruijo = x.to_f
  kaijo = 1
  sign = 1
  kou = ruijo
  taylor = kou
  loop.with_index(1) do |_,n|
    ruijo *= x**2
    kaijo *= (2*n)*(2*n+1)
    sign = -sign
    kou = ruijo * sign / kaijo
    return taylor if kou.abs <= Float::EPSILON
    taylor += kou
  end
end

どうにも気持ち悪い。何が気持ち悪いかって、結局は、各項を生成するループに、各項を足しあわせていくコードが混ざり込んでるのが気持ち悪い。でも、漸化式を回しながら項を生成しているから、普通に関数を作ってループを分離することはできない。

さて、ここでFiberを使えばと思った人も多いはず。フィボナッチ数列の生成に利用するサンプルよく見るし。実際初め僕も使ってみて、かなり綺麗になった。コルーチン素晴らしい。

def taylor_sinx_2(x)
  gen = Fiber.new do
    ruijo = x.to_f
    kaijo = 1
    sign = 1
    loop.with_index(1) do |_,n|
      Fiber.yield ruijo * sign / kaijo
      ruijo *= x**2
      kaijo *= (2*n)*(2*n+1)
      sign = -sign
    end
  end
  taylor = 0; kou = 0
  taylor += kou while (kou = gen.resume).abs > Float::EPSILON
  taylor
end

綺麗だけど、while微妙。inject使いたい。無限リストに出来無いの?それEnumerator.newで出来るよ!

def taylor_sinx_3(x)
  kous = Enumerator.new do |y|
    ruijo = x.to_f
    kaijo = 1
    sign = 1
    loop.with_index(1) do |_,n|
      y << ruijo * sign / kaijo
      ruijo *= x**2
      kaijo *= (2*n)*(2*n+1)
      sign = -sign
    end
  end
  kous.inject{|sum,kou|kou.abs>Float::EPSILON ? sum+=kou : (break sum)}
end

見事に遅延評価無限リストっぽいものが出来た。FiberやEnumeratorを使うと漸化式や足し合わせる部分だけを意識してコードを書けるので、見た目が綺麗になるだけでなく、すんなりと混乱せずに書けるというのも嬉しかった。コルーチンは無限リストだけじゃなく様々な用途がある強力なフロー制御だと思うけど、ジェネレータが欲しいときにはEnumerator.newがおいしい場合が多そう。

ほんの少し、より抽象的に書いてみる。

def taylor_sinx_4(x)
  kous = Enumerator.new do |y|
    ruijo = x.to_f
    kaijo = 1
    sign = 1
    calc = ->{ruijo * sign / kaijo}
    y << calc.call
    loop.with_index(1) do |_,n|
      ruijo *= x**2
      kaijo *= (2*n)*(2*n+1)
      sign = -sign
      y << calc.call
    end
  end
  kous.take_while{|kou|kou.abs>Float::EPSILON}.inject(:+)
end

take_whileは関数型っぽくて凄くかっこいい。ただ、実際使いどころがそんなにあるのだろうか…という気もする。

Haskellなんかだと、変数を書き換えて漸化式を回すのではなくその部分も無限リストにするのだろうし、こういう手続型と関数型が奇妙に混じったコードはRubyらしいと思う(Pythonもそうかな)。

blog comments powered by Disqus