やりたいこと
関数型プログラミングの影響を受けて、
- 無限リストから条件を満たす値を得るような、宣言的実装をRubyで書いてみたい。
- 計算結果を次の計算に用いるような計算を、無限長のリストを生成する形で実装してみたい。
やったこと
ブロックを遅延評価で無限回実行するための方法を調べました。
単純に書くなら、
loop.lazy.collect(&block)
繰り返し回数が入力として欲しい場合は、
(0..Float::INFINITY).each.lazy(&block)
と書けます。
loop
は制御構文だと思い込んでいたのですが、制御構文のページに記述が無いと思ったら、Kernelのモジュール関数でした。
できるようになったこと
3のn乗数で10以上の値を5つ取得するまで計算して表示するプログラムはこんな感じに書けました。
lmap = -> {
acc = 1
loop.lazy.collect{ acc *= 3 }
}.call
p lmap.select{ |x| x >= 10 }.take(5).force
#=> [27, 81, 243, 729, 2187]
アキュムレータとなる変数が手軽に欲しい時は、procでスコープを区切っておくと少しだけ安全かもしれません。
言いたいこと
ここからが趣旨です。
手続き的ではなく、宣言的に書くための考え方を考えてみた
「どうするか」(手続き)ではなく、「どうなっていてほしいか」(宣言)を書くと、簡潔に書ける場合があります。
何かを計算して求める処理を宣言的に書くためには、
- 候補となる値の集合(を生成する式)を書き、
- 集合から求めたい値を抽出する。
という発想が必要になりそうです。より詳しい説明については、
- るびま41号の記事「無限リストを map 可能にする Enumerable#lazy」
- るびま34号の記事「Fiber と Proc ―― 手続きを抽象化する二つの機能」
- るびま38号の記事「map と collect、reduce と inject ―― 名前の違いに見る発想の違い」
が分かりやすかったのでオススメです。
実装にあたっては、Rubyリファレンスマニュアルの
- Enumerator::Lazyクラスのページ http://docs.ruby-lang.org/ja/2.2.0/class/Enumerator=3a=3aLazy.html
- Enumerableモジュールのページ http://docs.ruby-lang.org/ja/2.2.0/class/Enumerable.html
- Enumeratorクラスのページ http://docs.ruby-lang.org/ja/2.2.0/class/Enumerator.html
は目を通しておくと良いでしょう。集合に対する便利な操作が目白押しです。
手を動かして考える
例えば、「2から始まる偶数の数列を指定した個数だけ含む配列を生成する」という処理を書くとします。
これを手続き的に書くと、一例として、
- 結果を返すための空の配列を用意して、
- 指定された回数ループを回し、
- ループ回数に1を足したものを2倍した値を毎回計算して、
- 結果を返すための配列に計算結果を追加していき、
- ループが終わったら配列を返す。
となります。コードは、
def calc_evens(count)
result = []
count.times do |n|
result << (n + 1) * 2
end
result
end
p calc_evens(5)
#=> [2, 4, 6, 8, 10]
こんな感じに書けます。
これを宣言的に書くため、
- 1から始まる等差1の無限数列から、
- 偶数だけを選んで、
- 値を5つ取って返す。
という形で書き下してみます。
def calc_evens(count)
lmap = (1..Float::INFINITY).each.lazy
lmap.select{ |x| x.even? }.take(count).force
end
p calc_evens(5)
#=> [2, 4, 6, 8, 10]
どうでしょう。手続き的なコードより、意図が明白で分かりやすくなった、と感じませんか?
Enumerator::Lazy#select あるいは Enumerable#select を用いることで、複雑な抽出条件でも記述することができます。
別の書き下し方を考えてみます。
- 1から始まる等差1の無限数列に対し、
- それぞれを2倍した数列を得て、
- 値を5つ取って返す。
def calc_evens(count)
lmap = (1..Float::INFINITY).each.lazy.map { |n| n * 2 }
lmap.take(count).force
end
p calc_evens(5)
#=> [2, 4, 6, 8, 10]
前の例と違い、集合を生成する記述が増え、代わりに抽出条件と選択が無くなりました。
求めたい値の集合だけを生成するのが簡単な場合、こちらの方が短く書けそうです。
逆に、求めたい値の集合だけを生成するのが難しい場合は、selectを使って抽出することを考慮すると良さそうです。
いずれにせよ、コードが分かりやすいかどうか、という点を意識することが大事かなと思います。
またまた、別の書き下し方を考えてみます。
- 2から始まる等差2の無限数列に対し、
- 値を5つ取って返す。
def calc_evens(count)
lmap = 2.step(Float::INFINITY, 2)
lmap.take(count)
end
p calc_evens(5).map(&:to_i)
#=> [2, 4, 6, 8, 10]
ここまで来ると好みの問題かもしれませんが、この書き方にはあるメリットがあります。
それは、開始する値の変更が直感的ということです。
def calc_evens(count, start_ = 2)
start = start_.even? ? start_ : start_ + 1
lmap = start.step(Float::INFINITY, 2)
lmap.take(count)
end
p calc_evens(5, 10).map(&:to_i)
#=> [10, 12, 14, 16, 18]
終わりに
- るびまを読もう!
- ツッコミ大歓迎です。