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

scramble cadenza

技術ネタのガラクタ置き場

ruby で最初難しかったけど、わかってきたこと【Enumerator(Generator)】

ruby

イントロ

最初理解するのが難しかったけど、理解できた気がするので書いてみるメモ。
お題は Enumerator と Generator について。
新しさ皆無の n番煎じです。

Generator

class Generator

最初に言ってしまうと、ruby 1.8 限定のクラスで、Generator は each メソッドを使えるようにする クラスです。(ざっくり言うと)
後で出てくる Enumerator と同じようなことをしてくれます。
まぁ今更 ruby 1.8.7 を使う人はいないとは思うんですけど、敢えて書いてみました。

どんな自作クラスを作っても、Generator を使えば each を使えるようになるので、そこからはやりたい放題だよね的なシロモノです。

使ってみる

NewString クラスを作ってみます。
NewString.new.generate.each とすると、String#each_char と同じ挙動をするラッパーです。(なんてめんどくさい!)

require 'generator'

class NewString
  def  generate
    Generator.new do |g|
      string.split('').each do |s|
        g.yield(s)
      end
    end
  end

  def string
    'scramble cadenza'
  end
end

generate メソッドで Generator#new を宣言しています。
Generator.new のブロック引数中で yield しているのが重要で、yield の引数がそのまま each を呼んだ時のブロック引数になります。

つまりどういうことでしょうか?
each のブロック引数は yield で渡した引数と同じ なのです。大事なので二度(ry

new_string = NewString.new
new_string.generate
#=> #<Generator:0x10914e6a0 ...>

new_string.generate.each{|s| p s} #=> このブロック引数 s は string.split('').each do |s| の s と同じもの。
"s"
"c"
"r"
"a"
"m"
"b"
"l"
"e"
" "
"c"
"a"
"d"
"e"
"n"
"z"
"a"
=> #<Generator:0x10925080 ...>

NewString#generate は Generator インスタンスを返し、 each メソッドを使用することができます。
だから NewString#generate 評価後は、まるで each を使ってるかの如く扱えるわけです。

まとめると Generator では以下の様なことができます。

  • 実際に each が呼び出せること
  • ブロック引数に yield で渡した引数が入っていること
  • yield で渡した引数は、次のループではArray で言う「一つ先」の要素が使われていること

ですがこれを応用するとブロック引数に 好きなオブジェクトを引き渡せる each を自作することが可能です。

今度はブロック引数が「レシーバーが二文字以上の時は先頭二文字、一文字しかない場合は一文字」となるようなクラスを作ってみましょう。(わかりづらい!)

class NewString2 < NewString
  def generate_two_char
    Generator.new do |g|
      string.scan(/\w{1,2}/).each do |s|
        g.yield(s)
      end
    end
  end
end

 NewString2.new.generate_two_char.each {|s| p s}
"sc"
"ra"
"mb"
"le"
"ca"
"de"
"nz"
"a"
#=> #<Generator:0x10914bc70 ...>

今度は正規表現で scan して、その結果を yield しています。 やりたいことできてますね。

余談

generator は ruby で書かれているので、ソース読んだほうが理解が早いです。
例えば Generator#each はたったこれだけしかやっていません。

def each
  rewind

  until end?
    yield self.next
  end

  self
end

Enumerator

class Enumerator

今紹介した Generator だと結構不便ですよね。

  • わざわざクラス(or メソッド)を作らなければいけない
  • generate メソッドを呼ばないといけない
  • 渡したい文字列もメソッドで定義する必要がある

思いつくところはこんなところでしょうか。
どれも厄介で、全ての条件を許容できる場合でないと適切ではないと言えます。

ですが 1.9 だとそんなことをせずとも、もっと簡単に each を使えるようになります。 それが Enumerator クラスです。

先ほどと同じことをやってみましょう。

e = Enumerator.new(str, :each_char)
e.each {|s| p s}
"s"
"c"
"r"
"a"
"m"
"b"
"l"
"e"
" "
"c"
"a"
"d"
"e"
"n"
"z"
"a"
=> "scramble cadenza"

e = Enumerator.new(str, :scan, /\w{1,2}/)
e.each {|s| p s}
"sc"
"ra"
"mb"
"le"
"ca"
"de"
"nz"
"a"
=> "scramble cadenza"

明らかにこちらのほうが使いやすいですね。

第一引数に each のレシーバーとなるオブジェクト、第二引数にメソッド名、第三引数以降に、第二引数で与えたメソッドの引数を渡すだけです。

もし指定したオブジェクトをブロック引数に渡したい場合は、Enumerator#new にブロックを渡します。
一年のうち、「月 + 日 の文字列」を each のブロック引数に渡すオブジェクトを作ってみました(例) 10月12日 => 1012) 。

e = Enumerator.new do |y|
  start  = Date.new(2013,1,1)
  finish = Date.new(2014,1,1)
  while start != finish do
    y << start.month.to_s + start.day.to_s
    start += 1
  end
end

e.each{|i| p i}
#=>
"11"
"12"
"13"
...
...
...
"1230"
"1231"

使い所は?

  • each のレシーバーのオブジェクトが動的に変わる場合とか。
  • ブロック引数に自分が指定したものを呼び出したい時とか。

あまり自分の中でも定まってないです。