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

nestした構造のprocを作る

# ok.
bl1 = proc { p "called!!" }
  #=> #<Proc:0x00636a14@(irb):1>
 
bl2 = proc { [1, 2].each(&bl1) }
  #=> #<Proc:0x00633788@(irb):2>
 
bl2.call
"called!!"
"called!!"
  #=> [1, 2]
 

ふつうは上記のような挙動をするが、

bl = proc { p "called!!" }
  #=> #<Proc:0x006369b0@(irb):1>
 
bl = proc { [1, 2].each(&bl) }
  #=> #<Proc:0x006337d8@(irb):2>
 
bl.call
SystemStackError: stack level too deep
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
... 6167 levels...
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):2:in `each'
        from (irb):2
        from (irb):3:in `call'
        from (irb):3

これは、procが実行されるときには、同じスコープ内でblが書きかわっているため、無限ループしてしまう。

これがどういうときに必要になってくるかというと、

procs = [ proc { p "called!!" } ]
  #=> [#<Proc:0x00636028@(irb):1>]
 
3.times do |i|
 procs[i + 1] = proc { (0..1).each(&procs[i]) }
end

のようなループの中でprocを作りその中で動的に作ったprocをつくるような場合だったりする。

いちおうevalを使うと、うまく実行させることもできる。

procs = [ proc { p "called!!" } ]
  #=> [#<Proc:0x0062979c@(irb):9>]
 
3.times do |i|
  procs[i + 1] = eval <<-EOE
    proc { [1, 2].each(&procs[#{i}]) }
  EOE
end
  #=> 3
 
procs.last.call
"called!!"
"called!!"
"called!!"
"called!!"
"called!!"
"called!!"
"called!!"
"called!!"
  #=> [1, 2]

しかし、evalはなるべくなら使いたくなかったので、#rails-tokyo@freenodeで聞いてみると、id:moroさんに教えてもらえた。

3:11 walf443: http://gist.github.com/20388
23:12 walf443: proc内でprocを実行させまくろうとしたら上のような罠にハマったのですが、
23:12 walf443: もっと良い回避方法を御存じの方いらっしゃいませんか?
23:13 morohashi: 同じ名前なのが行けないんじゃないでしたっけ
23:14 walf443: ですね。同じ名前なのがまずいのでevalでむりやり回避してます
23:15 morohashi: bl1とbl2の中身は無関係なんですか? なんつーか、プログラマブルに作れます??
23:17 walf443: うーん。とループの中でprocを順番にネストした構造にしたいというのが目的ですかね。
23:18 morohashi: finder = [{:cond=>"hoge"},{:cond=>"fuga"].inject(proc{ find(1) }){|p,c| with_scope(c, &p) }
23:18 walf443: プログラマブルに作れるというのは意味がちょっとよくわかってないです
23:18 morohashi: とか昔書いた気が。
23:18 morohashi: あー、ループの中で、というのであってると思います。
23:19 morohashi: こんな感じで解決するのなら私が推測した問題があってたということで
23:20 walf443: with_scopeってactivesupportでしたっけ?
23:20 morohashi: あーこれはRails 2.0のころに書いたコードです。named_scope入るまえ。
23:21 morohashi: named_scopeはこれをメソッドチェーンで出来るようになってる凄い子
23:21 walf443: ふむ.
23:24 walf443: injectだと一時変数がブロックローカルスコープ内におさまっているので、名前の重複を起こす心配がないので大丈夫なのかな。
23:24 walf443: ちょっとinjectで試してみます.
23:31 walf443: うーん。 http://gist.github.com/20388
23:31 walf443: 最後の一回しか実行されてないっぽいですねぇ。。。
23:47 morohashi: (0..2).inject(proc{p :called}){|pr,i| proc{(0..i).each(&pr)} }[]
23:47 morohashi: でいっぱい動きますよ
23:48 walf443: 一番最後みたいに、
23:48 walf443: http://gist.github.com/20388
23:48 walf443: メソッド呼びだしにすると動きますね。
23:48 morohashi: てか [0..1]
23:49 morohashi: じゃなくて[0..i]か
23:50 morohashi: [ 0..1 ].each(&b)でなく(0..1).each(&b)じゃないでしょうか。
23:50 walf443: うはw
23:51 walf443: Range派じゃないのがバレたw
23:51 morohashi: www
23:52 walf443: このパターンのときはinjectを使わざるをえないということは肝に明じておきますw
23:52 walf443: morohashi++
23:54 walf443: ちなみにblogのエントリにしようと思うのですが、IRCのログ出してもOKですかね? > morohashiさん
23:56 morohashi: もちろん
23:56 walf443: あざっす

injectは使うとコードは一見してわかりにくくなるし、eachで実行するのにくらべて非常に遅くて、最近はあまり好きじゃなかったのですが、この場合はinjectを使ってブロックローカルスコープを作り出すのがよさそうですね。

id:moro++