Ruby block/proc/lambdaの使いどころ

  • 1146
    いいね
  • 3
    コメント

(2015/10/13追記)
今なら、他言語には無名関数やcallback関数というものがありますねとか、イベント駆動の世界を覗いてから戻ってくるとより腑に落ちるかもしれませんとか、もう少し全体観の中で説明する気がしますが、当時は本記事の様な理解が役に立ったことは事実なので、引き続き公開を続けます。

(2013/11/29追記) block_given? について

Twitter上で「Kernel.#block_given?についての解説があってもよさそう」と
指摘を頂きましたので、本文下部に追記しました。

概要

Ruby on Rails Tutorialのエッセンスを自分なりに整理してみる4

Railsを触る際知っていると便利なRubyの基礎 [ブロックとかシンボルとか]
http://qiita.com/kidachi_/items/46a6e49b6306655ccd64
の続き。

Ruby on Rails Tutorial(chapter4)
http://railstutorial.jp/chapters/rails-flavored-ruby?version=4.0#fnref-4_11

流れ

  • ブロックとは何か
  • yield、Procとは何か
  • Proc補足
  • で、ブロックやProcって何が嬉しいの?

ブロックとは何か

ひとことで言うと

do~end(もしくは{~})で囲われた、引数となるためのカタマリ。

ブロックについて調べようとするとだいたい「yield(いーるど)」とか「Proc」とかが
混ざってきてよく分からなくなるけど、実際はこれだけです。

yieldは「ブロックを呼び出すもの」、Procは「ブロックをオブジェクト化したもの」であり、
ブロック自体とは別物(詳しくは後述)。

ブロックの性質

  • それ単体では存在できず、メソッドの引数にしかなれない
    • 「do~endのカタマリ」がその辺に単体で転がっているのは見た事ないはず。
  • 引数として渡されたブロックは、yieldによって実行される
#ブロックを受け取るメソッドの定義
def give_me_block
  yield
end

#メソッドの引数としてブロック(do~end)を渡して、実行
give_me_block do
  p "Hello, block!"
end
=> "Hello, block!"    #give_me_block内で、yieldによって呼び出された。

ただ、yieldというのはあるべき手順をすっ飛ばしているため分かりづらい。
以下の流れを追っていくと理解しやすいです。

「そもそもブロックとは引数であること」を明示した書き方も存在する

#メソッド定義
def give_me_block( &block )
  block.call
end

#実行
give_me_block do
  p "Hello, block!"
end
=> "Hello, block!"

def give_me_block( &block )で、ブロック引数を受け取ることを明示している。

&blockの「&」って?

&を付けることで、実際に引数にブロックが渡ってきた際、
Procオブジェクトに変換している。

Procオブジェクトって?

  • ブロックをオブジェクト化したものがProc
    • ブロックがそれ単体では存在できないことを思い出す (→オブジェクト化してしまえばok)
    • ブロックをオブジェクトに変換することで、引き渡されたメソッド(give_me_block)内で扱えるようにする
  • Procオブジェクトは、callで呼び出すことが出来る
    • 2行目のblock.callの正体はこれ。

ちなみに、ブロック引数は、仮引数の中で一番最後に定義されていなければならない

以下みたいなのはNG

  • def give_me_block( &block1, param1 )
  • def give_me_block( &block1, &block2 )

つまるところ引数として渡せるブロックは一つだけ

以上を踏まえると

こんな考え方も出来る。

「どうせブロック引数は一つしか取れないんだから

呼び出し箇所をblock.callなんて明示せずに、yieldで統一しちゃえば良いじゃん」

#メソッド定義
def give_me_block( &block )
  yield    # block.callをやめてyieldに変更
end

#実行
give_me_block do
  p "Hello, block!"
end
=> "Hello, block!"

さらに

「ブロックは全部yieldが示すんだから、仮引数(&block)もいらなくね?」

#メソッド定義
def give_me_block    # (&block)を除去
  yield
end

#実行
give_me_block do
  p "Hello, block!"
end
=> "Hello, block!"

これが色々省略されまくったyield文の正体です。

一見give_me_blockは何も引数をとらないメソッドのように見えるのに、
その内部にyieldを持っている以上「ブロックを引数として受けとるメソッドである」
ということを認識しないといけない。

Rubyコードを読み解く際によく注意しておかなければならない点の一つのみたいです。

以下記事でまつもとさんが話しているのもそんな話。
http://itpro.nikkeibp.co.jp/article/COLUMN/20070621/275509/?P=3
「もし最初から暗黙の引数みたいなデザインをしていたら、
今許しているアンパサンド「&」の文法は必須にして、yieldはなくして、
「&」が付いていないのにブロックを渡したらエラーになるとか…」

Proc補足

上でも説明していますが補足含めて。

  • ブロックをオブジェクト化したものがProc
  • Procオブジェクトはcallで呼び出すことが出来る
  • Procに引数を期待する書き方も出来る
    • 「メソッドの引数となるProcにさらに引数が期待できる」って混乱しそうですが・・
  • Proc.newとlambdaはほぼ同義

Procオブジェクトの定義と呼び出しサンプル

#インスタンス生成
proc1 = Proc.new do
  p "hoge"
end

#実行
proc1.call
=> "hoge"

呼び出される(callされる)際に引数が渡されることを期待する書き方サンプル

#インスタンス生成
proc2 = Proc.new do |s|    #sが仮引数
  p "Hello, #{s}!"
end

#実行
proc2.call("Proc")    #"Proc"が実引数
=> "Hello, Proc!"

Proc.newとlambdaはほぼ同義

以下の記述で上の2例と同じ結果が得られる

#インスタンス生成
lambda1 = lambda { p "hoge" }
#実行
lambda1.call
=> "hoge"

lambda2 = lambda { |s| p "Hello, #{s}!" }
lambda2.call("Proc")
=> "Hello, Proc!"

※細かくはProcとlambdaには違いが存在しますが、ここでは触れません。
補足は以下等を参照ください。

RubyでlambdaとProcの違いは?
http://qa.atmarkit.co.jp/q/68

で、ブロックやProcって何が嬉しいの?

一つ目は

メソッドを後々の利用時に柔軟に拡張出来る

例えば、開発者Aが「5という数で好きな事をしてもらうメソッドを作ろう」と
考えた場合、以下のようになります。

def magic_five_box(after_input, someProc)
  someProc.call(5, after_input)
end

開発者B「私は5を使って足し算します」

sum_proc = Proc.new do |x, y|
  p x + y
end

magic_five_box(3, sum_proc)
=> 8

開発者C「私なら文字列を5回表示させます」

string_proc = Proc.new do |x, string|
  p string * x
end

magic_five_box('happy_proc! ', string_proc)
=> "happy_proc! happy_proc! happy_proc! happy_proc! happy_proc! "

このメソッド例は大した意味は持ちませんが、
使い方によって有効に利用できそうな雰囲気が伝わったでしょうか。

下の記事はブロックのことを「メソッドに対するメソッドのMix-in」と表現しています。

Rubyのブロックはメソッドに対するメソッドのMix-inだ!
http://melborne.github.io/2008/08/09/Ruby-Mix-in/

よく考えてみると、Ruby標準のeachやmapなどもブロックを受け取るメソッドです。

「配列を扱うならこれ、という『土台』は用意しておくから、
細かい部分はブロックを加えて自由に料理して」という思想ですね。

状態を持った関数(クロージャ)としての機能が得られる

ブロック/Proc二つ目のメリットはこちら。

n = 1
proc = Proc.new do
  n = n + 1
end

proc.call
=>2
proc.call
=>3
proc.call
=>4

Procオブジェクト(proc)は外側のスコープに存在するnへの参照を保持し、
呼び出す度にnをインクリメントできるメソッドができました。

が、クロージャの細かい特徴/用途となるとまた長い話になるので、
詳しくは以下等を参照してみてください。

私が今までクロージャを理解できなかった理由Add Starfoussin
http://d.hatena.ne.jp/artgear/20120115/1326635158

(2013/11/29追記) Kernel.#block_given? について

block_given?とは

名前の通り、引数としてブロックが与えられたかどうかを判別するメソッド。

利用例

開発者A
「7という数で好きな事をしてもらうメソッドを作ろう。
でも、もしブロックが渡されなくても使えるようにしよう。

def wonder_seven_box
  if block_given?
    yield(7)
  else
    p "Don't mind. Feel free to call me."    # ブロックが与えられなければこちら
  end
end

開発者B「私は7を使って足し算します」

wonder_seven_box do |x|
  p 3 + x
end
=> 10

開発者C「私なら文字列を7回表示させます」

wonder_seven_box do |x|
  p "happy_block! " * x
end
=> "happy_block! happy_block! happy_block! happy_block! happy_block! happy_block! happy_block! "

開発者D「ブロックよく分からない」

wonder_seven_box
=> "Don't mind. Feel free to call me."

ブロックを与えなくても呼び出すことが出来ました。

block_given?を整理していて気になった事

※本項はRubyの言語仕様に関する疑問点の備忘録です。
「ブロックとProcをちゃんと理解する」からは少しずれますので、
不要であれば読み飛ばして頂ければと思います。

細かいけど

お気づきかと思いますが、magic_five_boxの例ではProcベースでしたが、
今回のwonder_seven_boxはブロックベースとなっています。

というのも、proc_given?のようなメソッドが見当たらなかったので。

つまり、procの場合は、

def magic_five_box(after_input, someProc)
  someProc.call(5, after_input)
end

の例が示す通り、Procオブジェクトを受け取る前提の記述となるため、
block_given?に類する判定がそもそも必要ない。

一方でブロックの場合は

def wonder_seven_box
  if block_given?
    yield(7)
  else
    p "Don't mind. Feel free to call me."    # ブロックが与えられなければこちら
  end
end

上記例で見た通り、
ブロックを引数に取っても取らなくても良い、より柔軟なメソッド
を作ることが出来ます。

ただ、まつもとさんが仰っている内容(以下再掲)を鑑みた時、ひとつ疑問が生まれました。

http://itpro.nikkeibp.co.jp/article/COLUMN/20070621/275509/?P=3
「もし最初から暗黙の引数みたいなデザインをしていたら、
今許しているアンパサンド「&」の文法は必須にして、yieldはなくして、
「&」が付いていないのにブロックを渡したらエラーになるとか…」

つまり以下の記述をスタンダードにした方が良いかも、ということですね。

def give_me_block( &block )
  block.call
end

そしてこの記述の場合、ブロックは渡されて来ることが前提となりそうです。

(もちろん、

  • 引数にブロックを期待する場合のみ、引数が渡されないケースも許容する

(メソッド内でblock_given?で判定)

というデザインもあり得るとは思いますが、
通常のメソッド定義と見た目はほぼ一緒なのにルールだけ違うという状況になってしまいます。)

何が気になっているのかというと、

本来 Kernel.#block_given? は必須の実装なのか?

ということ。実は

(yieldという特殊な存在に引き摺られて)後付けで実装された

ものであり

想定外の(もっと言うと本来不要な)柔軟性を生み出している

ものだったりしないのでしょうか。

→仮に通常の引数に対してblock_given?に類するものがあったとすると、
 こんなことをしてる訳で。

def test_method ( args )
  if args_given?
    hoge
  else
    fuga
  end
end

果たしてこれをポジティブに「柔軟」と捉えるべきか。

ご相談

もしこのあたりの成り立ちのストーリーに詳しい方、
または言語デザインに対するご指摘をお持ちの方がいらっしゃいましたら
ぜひご教示頂けると嬉しいです!

※同時に、Ruby初心者の考察であるため、的を外している点ありましたら
突っ込みを頂けますと幸いですm(_ _)m

ブロック/Proc参考&御礼

数あるブロックの記事を読んでもいまいちすっきりできなかった自分でしたが、
以下の記事で目から鱗が落ちました。

Rubyの動かないコード(初級編)ブロックとクロージャの性質
http://d.hatena.ne.jp/language_and_engineering/20101118/p1

分かりやすい記事をありがとうございました!

以下に続く

[Rails] TwitterBootstrapとSassとAssetPipeline
http://qiita.com/kidachi_/items/4c47b28b2a74c723d835