ruby で最初難しかったけど、わかってきたこと【Enumerator(Generator)】
イントロ
最初理解するのが難しかったけど、理解できた気がするので書いてみるメモ。
お題は Enumerator と Generator について。
新しさ皆無の n番煎じです。
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
今紹介した 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 のレシーバーのオブジェクトが動的に変わる場合とか。
- ブロック引数に自分が指定したものを呼び出したい時とか。
あまり自分の中でも定まってないです。