エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Rubyでunfoldを書く

特定の値から出発して演算を繰り返し値の並びを出力する unfold 。 そういえば Ruby に unfold ってないんだっけ? というのが発端。

unfold とは

早い話が fold の逆です。

Elixir では Stream.unfold/2 が定義されています。

# 1 から始めて、前の値に 1 を加える
Stream.unfold(1, fn x -> {x, x + 1} end) |> Enum.take(10)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 1 から始めて、前の値を 2 倍にする
Stream.unfold(1, fn x -> {x, x * 2} end) |> Enum.take(10)
#=> [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
# フィボナッチ数列
Stream.unfold({1, 1}, fn {n1, n2} -> {n1, {n2, n1 + n2}} end) |> Enum.take(10)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Stream.unfold/2 自体は終端を定義しません。 Enum でなく Stream で定義されているのもそのためです。 Enum.take/2 などで必要な分を取り出す必要があります。

Ruby で unfold を書く(終端を条件で判断するばあい)

最初に思いつくのは、繰り返しの中で yield を使い、結果を集める方法。 しかし無制限にいつまでも繰り返すわけにはいかないので、終了条件を織り込まないとなりません。

実装するとこんな感じ。

def unfold(x)
  result = []
  loop do
    y, x = yield x
    break if x.nil?
    result.push y
  end
  result
end
# 1 から始めて、10 以下の範囲で前の値に 1 を加える
unfold(1) do |x|
  [x, x + 1] if x <= 10
end
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 1 から始めて、512 以下の範囲で前の値を 2 倍にする
unfold(1) do |x|
  [x, x * 2] if x <= 512
end
#=> [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
# 55 以下の値のフィボナッチ数列
unfold([1, 1]) do |n1, n2|
  [n1, [n2, n1 + n2]] if n1 <= 55
end
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Ruby で unfold を書く

しかし冒頭の Stream.unfold/2 の例のように決められた個数の要素を取り出すようなばあいにはこれでは不便です。

そこで「Ruby unfold」で検索すると、簡単にヒットするのがこちらのコード。

実のところ、これがほぼ正解っぽいです。

Enumerator.new のブロックの引数として渡される Enumerator::Yielder オブジェクトの存在も初めて知りました。

docs.ruby-lang.org

def unfold(x)
  Enumerator.new do |yielder|
    loop do
      y, x = yield x
      yielder << y
    end
  end
end
unfold(1) { |x| [x, x + 1] }.take(10)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
unfold(1) { |x| [x, x * 2] }.take(10)
#=> [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
unfold([1, 1]) { |x, y| [x, [y, x + y]] }.take(10)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

この実装でしたら最初に書いた終端を条件で判断するケースにも対応できます。

unfold([1, 1]) { |x, y| [x, [y, x + y]] }.take_while { _1 < 100 }
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Elixir の Stream.unfold/2Enum.take_while/2 を使って同じように書けます。

Stream.unfold({1, 1}, fn {n1, n2} -> {n1, {n2, n1 + n2}} end) |> Enum.take_while(& &1 < 100)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

モジュールにしてみた

繰り返し利用できるようにモジュールを定義してみました。 が、今ひとつ。 定義の仕方がうまくないというのもありますが、初期値と操作の結びつきが強いので、任意の操作を受け取れるようにしても使い勝手がよくないということなのかもしれません。

module Unfoldable
  def unfold
    x = self
    Enumerator.new do |yielder|
      loop do
        y, x = yield x
        yielder << y
      end
    end
  end
end
Integer.include(Unfoldable)
1.unfold { |x| [x, x + 1] }.take(10)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a = [1, 1]
a.extend(Unfoldable)
a.unfold { |n1, n2| [n1, [n2, n1 + n2]] }.take(10)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]