Rubyにおいてシーケンス、つまり意味的に連続する要素の並びを簡単に生成するObject#repeatというものを考えましたよ!以前に考えたEnumerable#repeatを単にすべてのオブジェクトに拡張したものですけど。

class Object
  def repeat(init=true, &blk)
    x = self
    Enumerator.new do |y|
      y << x if init
      loop { y << (x = yield x) }
    end
  end
end

repeatは、そのレシーバオブジェクトを初期値として、渡されたブロックを繰り返し適用します。適用の結果はEnumeratorオブジェクトでラップされているので、遅延評価されます。

以下に、問題に答える形で使い方を見せますね。比較のためrepeatを使わない方法も適宜示します。

1. 初項1、公差2の等差数列の最初の20項を求めなさい。

repeatを使うと等差数列は次のように書けます。

1.repeat { |x| x + 2 }.take(20) # => [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]

ブロックで公差を足していくだけです。

repeatを使わない場合は、Integer#stepを使うんでしょうね。

1.step(Float::MAX.to_i, 2).take(20) # => [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]

簡潔ですが、Float::MAX.to_iというのがちょっとトリッキーですね。

2. 初項3、公比2の等比数列の最初の20項を求めなさい。

次に等比数列です。

3.repeat { |x| x * 2 }.take(20) # => [3, 6, 12, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144, 12288, 24576, 49152, 98304, 196608, 393216, 786432, 1572864]

極めて自然に書けます。

repeatを使わない場合は、どう書くんですかね。

x = 3
n = 1
res = []
while n <= 20
  res << x
  x = x * 2
  n += 1
end
res  # => [3, 6, 12, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144, 12288, 24576, 49152, 98304, 196608, 393216, 786432, 1572864]

一応答えは出ますが、Rubyistはこんなことしませんよね。次のほうがもう少しマシでしょうか。

x = 3
[3] + 19.times.map { x = x * 2 } # => [3, 6, 12, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144, 12288, 24576, 49152, 98304, 196608, 393216, 786432, 1572864]

3. 1で求めた等差数列がその第1階差数列となるような数列を求めなさい。

階差数列は、数列の隣合う項の差から生成される数列です。わかりにくいので答えを先に言うと、[1, 2, 5, 10, 17..]が解です。この数列の各項の差が、[1, 3, 5, 7…]となって、1で求めた数列と等しいことが分かると思います。

repeatを使うと一式で階差数列を作ることができます。

[1, 1].repeat { |a, b| [a+2, a+b] }.take(20).map(&:last) # => [1, 2, 5, 10, 17, 26, 37, 50, 65, 82, 101, 122, 145, 170, 197, 226, 257, 290, 325, 362]

配列の先頭位置で順次公差2の等差数列を作っていき、配列の2項位置で階差数列を作っていきます。最後に2項だけを取り出せばOKです。

repeatを使わない場合、等差数列を作った上でinjectするんでしょうか。

seq = 1.step(Float::MAX.to_i, 2).take(20) # => [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]

y = seq.first
seq.inject([y]) { |m, x| m << y = x + y } # => [1, 2, 5, 10, 17, 26, 37, 50, 65, 82, 101, 122, 145, 170, 197, 226, 257, 290, 325, 362, 401]

4. フィボナッチ数列の最初の20項を求めなさい。

みんな大好きフィボナッチです。repeatで書くと次のようになります。

[0, 1].repeat { |a, b| [b, a + b] }.take(20).map(&:first) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

初項0,1を配列として渡すことで、簡潔に書けます。

repeatを使わない場合はどうでしょう。

a, b = 0, 1
[0] + 19.times.map { a, b = b, a + b }.map(&:first) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

なかなか簡潔ですが、初項の処理が気になるといえば、気になります。

5. トリボナッチ数列の最初の20項を求めなさい。

フィボナッチが先行する2項の和を項の値とするのに対して、トリボナッチは先行する3項の和を値とする数列です。repeatを使って。

[0, 1, 1].repeat { |a, b, c| [b, c, a + b + c] }.take(20).map(&:first) # => [0, 1, 1, 2, 4, 7, 13, 24, 44, 81, 149, 274, 504, 927, 1705, 3136, 5768, 10609, 19513, 35890]

いいですね。

repeatを使わない例。

a, b, c = 0, 1, 1
[0] + 19.times.map { a, b, c = b, c, a + b + c }.map(&:first) # => [0, 1, 1, 2, 4, 7, 13, 24, 44, 81, 149, 274, 504, 927, 1705, 3136, 5768, 10609, 19513, 35890]

6. ニュートン法を使って5の平方根を求めなさい。

ニュートン法でnの平方根を求めるときは、任意の近似値xを選び、xとn/xの平均を取ってより良い近似値xを得ます。これを繰り返し十分に良い近似値が得られたら処理を終えるようにします。

loopを使った解法は、次のようになります。

a = 5
x = 1.0
eps = 0.0001
loop do
  y = x
  x = (x + a/x) / 2.0
  break x if (x - y).abs < eps
end
x # => 2.236067977499978

一方で、repeatを使うと次のように書けます。

a = 5
eps = 0.0001
1.0.repeat { |x| (x + a/x) / 2.0 }
   .each_cons(2)
   .detect { |a, b| (a - b).abs < eps }[1] # => 2.236067977499978

7. Aから始まるExcelの列名ラベルを60個生成しなさい。

repeatを使わない場合は、次のようになるでしょうか。

c = '@'
60.times.map { c = c.succ } # => ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB", "AC", "AD", "AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP", "AQ", "AR", "AS", "AT", "AU", "AV", "AW", "AX", "AY", "AZ", "BA", "BB", "BC", "BD", "BE", "BF", "BG", "BH"]

repeatを使うと、より簡潔に書けます。

'A'.repeat { |x| x.succ }.take(60) # => ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB", "AC", "AD", "AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP", "AQ", "AR", "AS", "AT", "AU", "AV", "AW", "AX", "AY", "AZ", "BA", "BB", "BC", "BD", "BE", "BF", "BG", "BH"]

8. 2進数10110011を10進変換しなさい(String#to_iを使ってはいけない)。

repeatを使わない例。

b = 10110011
sum = 0
n = 0
while b >= 1
  b, r = b.divmod(10) 
  sum += r*2**n
  n += 1
end
sum # => 179

repeatを使って。

digit = 1.repeat { |x| x*2 }

10110011.repeat(false) { |m,| m.divmod(10) }.take(8)
        .map { |_, i| i*digit.next }.inject(:+) # => 179

あまり簡潔じゃないですね^ ^;

9. ランダムなブール値の並び20項を生成しなさい。但し、trueが連続してはいけない。

repeatを使わないと、ローカル変数で前の値を保持して、次のように書くのでしょう。

prev = true
seq = 20.times.map do
  prev = prev ? false : [true, false].sample
end
seq # => [false, true, false, false, true, false, true, false, false, true, false, true, false, true, false, true, false, false, false, false]

repeatを使うと、これは次のように簡潔に書けます。

true.repeat { |bool| bool ? false : [true, false].sample }.take(20) # => [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, false, false, true, false]

10. ランダムなブール値の並び20項を生成しなさい。但し、trueが3つ連続してはいけない。

repeatを使わない場合、先の例と同様にローカル変数で前の値2つを保持する必要があります。

prev = prev2 = true
seq = 20.times.map do
  tmp = prev
  prev = [prev, prev2].all? ? false : [true, false].sample
  prev2 = tmp
  prev
end
seq # => [false, false, false, false, true, false, false, true, false, true, true, false, false, false, true, true, false, false, false, false]

repeatを使うと、次のように書けます。

[true, true].repeat { |a, b| [ b, [a, b].all? ? false : [true, false].sample ] }.take(20).map(&:last) # => [true, false, true, true, false, false, true, false, false, false, true, false, false, true, false, true, true, false, false, true]

いいですね。

Object#repeatを紹介しました。なかなか便利そうですよね?また、調子に乗ってFeatureリクエストしちゃおうかな..


(追記:2012-07-14)

大変なことが起こりました..

この記事に対し昨日、Matzがつぶやいたのです。

matz says

このつぶやきを僕は、「名前さえ良ければRubyに採用してもいいよとMatzが言ってる」と解釈していいのですか?それとも、「昨日言ってくれれば良かったんだけどねー(つまり「おととい来やがれ」の婉曲表現)」と解釈すべきなのですか?

いや、真実はどうであれ、ここはポジティブに前者だと考えるべきでしょう。The Power of Positive Thinking.

名前重要

そうすると、名前です、なまえ。「名前重要」。もちろん個人的にはrepeatはなかなかな名前だと思っていたのですが、ここはまず、なぜrepeatがMatzにとって「名前がなあ」なのか考えてみます。

repeatという語は複数のプログラミング言語で使われており、それは概ねRubyにおけるloopと等価、つまり「単純繰り返し」のための機能を提供するもののようです。この点からすると、Object#repeatはその機能を知らないユーザにとっては、次のような機能を提供するものと想像される可能性があります。

1.repeat(5) # => [1, 1, 1, 1, 1]

つまりrepeatは単にレシーバオブジェクトを繰り返した結果を返すメソッドであると想像するのです。なるほど合理性があります。恐らくMatzもこのようなことを懸念したんだと想像します。

一方で、Object#repeatの本来の機能は「そのレシーバオブジェクトを初期値として、渡されたブロックを繰り返し適用する」ことで、より正確に言うならば、「レシーバオブジェクトを繰り返すのではなく、ブロック内手続きをレシーバオブジェクトに繰り返し適用する」メソッドなのです。

再考すると確かに、repeatではその機能を説明しきれていない感がありますね。

そこで、代替案を考えてみましたよ!すなわち、「繰り返し適用」を意味するrepeat_applyです。ちょっとメソッド名が長くなる点に不満はありますが、その機能をより的確に表していると言えるんじゃないでしょうか。

ということで、Object#repeatを取り下げ、Object#repeat_applyでどうでしょう?

って、ここで「どうでしょう?」っていっても何も進まないでしょうけど…。結果はどうであれ、機能的には評判が良い感じなので、Featureリクエストの準備します。ちなみにrepeat_callという案も第2候補として挙げておきます。


(追記:2012-07-20)

本日、Object#sequenceでFeatureリクエストを出しました。リクエストにおいては、コメントおよびツイートを通して頂いた名称のアイディアを共に記載しました。ありがとうございます。

Feature #6758: Object#sequence - ruby-trunk - Ruby Issue Tracking System


関連記事:

Rubyのrepeat関数でフィボナッチ、トリボナッチ、テトラナッチ!


リピートの法則 by 上妻 英夫



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS