読者です 読者をやめる 読者になる 読者になる

歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【Ruby】injectでリストに値を加えていくコードで、代わりにEnumerator::Lazyを使ってPythonのジェネレーター風の遅延評価を行う

会社のRubyistが「一つの言語を極めておくと、他の言語もゴリゴリ書けるようになるって最近Go言語書いてるPerl Mongerのオッサンが言ってた」って言ってました。

私も、Pythonをある程度書けるようになってたおかげで、RubyR言語でも迷わずにプログラミングできるようになっている気がします (まだまだ少しコーディングが遅い気はするのですが)。

この間の記事で書いたように、DBから取ってきたリストをjoinさせる関数を定義し、抽象度の高い楽しいプログラミングをしていました。

kiito.hatenablog.com

ただし、実装していたバッチ処理で、割と汚いデータを相手にしていたため、何度かに分けてjoinする必要があり、 その中でinjectする際に毎回ループを回してしまっています。

Pythonでは、このようなときはジェネレーター(遅延評価させるリスト)を使って、内部的には一回のループで済むようにします。 ちゃんとした説明を見たい人はEffective Pythonを読んでください。

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

Rubyでも同じように遅延評価できないかと思ったら、Enumerator::Lazyというクラスがありました。また、.map.selectメソッドの前に.lazyを挟むことで、することができるようです。

qiita.com

joinの関数のコードはややこしいので、↓のような問題を考えます。

# この配列から
nested_list = [[1], [2, '2'], ['a', 3, 'b'], [4], ['c']]
# 数字だけのリストが欲しいとする
number_list = [1, 2, 3, 4]

Pythonであれば、ジェネレーター式を使ってこんな感じで処理すると思います。

# Python3.5ならこんな感じ
def unlist(nested_list):
  for ary in nested_list:
    for elem in ary:
      if type(elem) is int:
        yield elem
      else:
        pass # 一応

gen = unlist(nested_ary)

# ジェネレーターオブジェクトができてる
print(gen)
# => <generator object unlist at 0x1021db708>

# 最初の要素を取り出す
print(next(gen))
# => 1

# リストにして残りの要素を取り出す
print(list(gen))
[2, 3, 4]

Rubyでは、同じような処理をinjectを使ってリストに追加していくことが多いように思います。 Pythonではyieldで値を1つずつ外に出していっているのに対し、injectで配列に突っ込んでいる感じですね。

number_list = nested_list.inject([]) do |ary, item|
  item.each do |x|
    ary << x if num.is_a?(Integer)
  end
  ary
end

# もちろん関数を作ることもできる
# Rubyではreturnが不要なので、そのまま包めばいいだけで楽
def unlist(nested_list)
  nested_list.inject([]) do |ary, item|
    item.each do |x|
      ary << x if num.is_a?(Integer)
    end
    ary
  end
end

puts number_list
# => [1, 2, 3, 4]

これを遅延評価させたいと思ったのですが、.lazyで呼び出す方法では、mapselectとその亜種がほとんどで、 Pythonのジェネレーター式のようにyieldしていくようなメソッドは無さそうでした。 injectも、そもそも畳み込みのためのメソッドなので遅延評価されません。

代わりにEnumerator::Lazyインスタンスを作ることで、ジェネレーター式と同じものを作ることができました。

# Ruby2.2で試しています
def lazy_unlist(nested_list)
  Enumerator::Lazy.new(nested_list) do |yielder, item|
    item.each do |x|
      yielder << x if x.is_a?(Integer)
    end
  end
end

generator = lazy_unlist(nested_list)

puts generator
# => #<Enumerator::Lazy:0x0055634bb03200>

# 最初の要素を取り出す
puts generator.first
# => 1

# リストに変換する
puts generator.force
# => [1, 2, 3, 4]
# Pythonと違って、一度使った値が消費されないみたいなので要調査

Pythonのジェネレーター式のようなものが、Rubyの柔軟な構文(後置のif文とか)を使った上で書けそうです。 Enumerator::Lazy.new(nested_list) do |yielder, item|じゃなくてnested_list.lazy.generate do |yielder, item|みたいな書き方が用意されててほしいなという気もします。

仕事のコードでは元々のinjectの書き方でも遅くなかったのでそのままですが、勉強になりました。 もう少し、PythonRubyの挙動の違いとかいろいろ試してみたい&調べてみたいですが。