yujiro's blog

エンジニアリング全般の事書きます

Proc の種類と使い分け 【Ruby】

Effective Ruby 第5章、項目34 「Proc の引数の個数の違いに対応できるようにすることを検討しよう」を読んで、Proc の知識が深まったのでアウトプットしてみます。


まず基本的な知識のおさらいです。

block をメソッド(関数)に渡します。

def hoge(&block)
  pp block.call("a")
end

hoge {|x| "#{x} was given." }

=> "a was given."

ブロックはメソッド(関数)側で & で受け取ると Proc オブジェクトになって、.call で呼べます。

block.call("a") のところは yield("a") でもOK。

対して、lambda は その場でProc オブジェクトになります。つまり、その場で呼べます。

irb(main):002:0> ->(x) {"#{x} was given."}.call("a")

=> "a was given."

逆にメソッドに対して、lambda を渡すことはできません。

def hoge(&block)
  pp block.call("a")
end

my_lambda = ->(x) {"#{x} was given."}

hoge my_lambda

=> `hoge': wrong number of arguments (given 1, expected 0) (ArgumentError)

当然ながら、その場でProcオブジェクトになり、変数に代入できているので、それをメソッドに渡すとフツーに引数としてみなされます。

そしたら、引数受取部の & をとってみましょう。

def hoge(block)
  pp block.call("a")
end

my_lambda = ->(x) {"#{x} was given."}

hoge my_lambda

=> "a was given."

いけましたね。

んで、次に「lambda はblock として渡せないの?(メソッド引数受取側で &blockのまま渡す)」ってなります。

これはメソッド呼び出し時に & をつければよいです。

def hoge(&block)
  pp block.call("a")
end

my_lambda = ->(x) {"#{x} was given."}

hoge &my_lambda

=> "a was given."

じゃあ、ブロックをメソッド側で & で受け取ったもの(block がProc オブジェクトになったもの)と、lambda は同じものなのかって気になりますよね。実際にみてみます。

def hoge(&block)
  pp block
end

hoge {|x| "#{x} was given." }

=> #<Proc:0x00007fedd70c2e30@(irb):12>

pp ->(x) {"#{x} was given."}

=> #<Proc:0x00007fedd7101388@(irb):2 (lambda)>

lambda のほうは (lambda) となっていますね。

これの違いは Effective Ruby では、

block(をProcオブジェクト化したもの) => 弱いProc

lambda => 強いProc

と言われていて、その違いは

引数の個数を厳密にチェックするかどうか

という所です。

lambda のほうはブロックを実行する際に、引数の個数が違っていれば、ArgumentError 例外を吐きます。

さきほどのコードで見てみましょう。

まず、blockです。

require "pp"

def hoge(&block)
  pp block.call("a")
end

hoge {|x, y| "#{x} and #{y} was given." }

=> "a and was given."

まぁフツーに nil になって string にキャストされてます。

次に lambda

pp ->(x, y) {"#{x} and #{y} was given." }.call("a")  

=>

proc.rb:26:in `block in <main>': wrong number of arguments (given 1, expected 2) (ArgumentError)
        from proc.rb:26:in `<main>

となりました。

ここまでの話を踏まえて、どう使い分けるかって所ですが、Effective Ruby では、「強いProc を渡したい状況で、実行時(.call時)に引数の個数が違っていたら例外が発生するから、そういう時は、Proc#arity メソッド使おうぜ」 って言っています。

強いProc を渡したい状況 というのは、Effective Ruby では、メソッドを渡すケースを説明されていました。さきほどの lambda を hoge &my_lambda としたケースを思い出してください。アレ実はメソッドオブジェクトでもOKなんです。

例えば、以下のようにすると

def hoge(&block)
  pp block.call("a")
end

hoge &"aiueo".method(:include?)

=> true

となります。

※ こんな実用例はないんですが、仕様だけ説明したいと思います

仕組みとしては、メソッドオブジェクトをProc に変換すると lambda(強いProc) になるということです。

def hoge(&block)
  pp block
end

hoge &"aiueo".method(:include?)

=> #<Proc:0x00007faf708c4070 (lambda)>

つまり、ここで、block.call("a", "b") となっていたら、例外になります。

このような、「メソッドオブジェクト(強いProc)を渡したいけど、メソッド側で .call するときにいくつ引数を渡すかを決めたくない」ときにProc#arity メソッドを使います。

def hoge(&block)
  pp "block引数の個数 : #{block.arity}"
  if block.arity == 1
    pp block.call("a")
  end
  if block.arity == 2
    pp block.call("a", "b")
  end
end

hoge &"aiueo".method(:include?)

=>

"block引数の個数 : 1"
true

こんな感じです。

まとめると、

  • Proc オブジェクトは 強いProc(lambda, メソッドオブジェクトをProcに変換したもの)弱いProc(block を Procに変換したもの) があり、強いProcは引数の個数が違かったら ArgumentError例外が発生する。
  • Proc#arity メソッドで Procオブジェクトが期待する引数の個数がわかる

ということですな。

Proc#arity の具体例はもっと実用的な例が Effective Ruby に載っているので興味あるかたは是非みてみてください。