以下のコードでyieldがどのように動作するか説明できますか?
def total(from, to)
result = 0
from.upto(to) do |num|
if block_given?
result += yield(num)
else
result += num
end
end
return result
end
total(1, 10) { |num| num * 2 }
上のコードが説明できる方は、この記事は読む必要ありませんのでブラウザバックしてください。
説明できない、意味がわからない方は安心してください。この記事を読めば上のコードが理解できるようになります。
この記事では、yield(ブロックの概念)を解説するとともに、実際の現場での使いどころを例を交えて解説します。
対象読者
・例文のコードの動きが理解できない人
・yield見たことあるけど、理解できていない人
・Ruby初心者~中級者の人
yieldを理解するためにまずはRubyのブロックについて理解する必要があります。
Rubyのブロックとは?
Rubyではすべてのメソッド呼び出しの際、引数と一緒に「処理のかたまり」も渡すことができます。
この「処理のまとまり」のことをブロックと呼びます。
eachやmapメソッドで目にしたことはあると思いますが、do ~ end または { ~ } の中に記載されている処理のかたまりがブロックです。
# do ~ end または { ~ } どちらの表記でもOK
[1, 2, 3].each do |i|
p i * 2
end
=> 1
2
3
[1, 2, 3].each { p i * 2 }
=> 1
2
3
この書き方はeachやmap専用の書き方ではなく、自身で実装したメソッドに対してもブロックを渡すことが可能です。
# block_methodは自身で実装したもの
block_method(10) { p 'Block called!!!'}
=> "Block called!!!"
10
ブロックを渡せることは分かりましたが、ブロックを渡されたメソッドはどのようにブロックを実行してるんでしょう?
ここで出てくるのがyieldです!
def block_method(num)
yield # 渡されたブロックを実行している正体
p num
end
block_method(10) { p 'Block called!!!'}
yieldを実行することでメソッドに渡されたブロックを実行することができます。
yieldとは?
まず、読み方は「イールド」と読み、英語の意味は、生じる、産出する、譲渡するなど様々な意味がありますが、Rubyだと「他のものに取って代わる」という意味がしっくりきます。
メソッドにブロックが渡されている場合、yieldを実行することでメソッドに渡されたブロックを実行することができます。
yieldはメソッドのように引数を渡すことも可能です。
yieldに渡された値はブロック記法において | と | の間にはさまれた変数(ブロックパラメータ)に代入され、ブロック内で参照することが可能となります。
以下の例だと、yieldに引数numを渡しており、ブロックのblock_paramという変数で参照しています。
def block_method(num)
yield(num)
p num
end
block_method(10) {|block_param| p block_param * 2 }
=>20
10
値の受け渡しは記法は違いますが、動きは概ね通常のメソッドと同じ考え方で良さそうです。
※注意
ブロックが渡されていない場合にyieldを実行すると、例外が発生します。
def block_method(num)
yield(num)
p num
end
block_method(10)
=> LocalJumpError (no block given (yield))
ブロックを渡したり渡さなかったり柔軟に対応したいケースでは、 block_given?で評価します。
block_given?はメソッドにブロックが引き渡されていればtrueを返し、ブロックが渡されていなければfalseを返します。
以下のように利用します。
def block_method(num)
yield(num) if block_given?
p num
end
# ブロックなし
block_method(10)
=> 10
# ブロックあり
block_method(10) { |block_param| p block_param * 2}
=>20
10
はい。以上でyieldの説明は終わりです。
ここまでくれば、最初に提示したコードがどんな動きをし、実行結果がどうなるかわかるかと思います。
def total(from, to)
result = 0
from.upto(to) do |num|
if block_given?
result += yield(num)
else
result += num
end
end
return result
end
total(1, 10) { |num| num * 2 }
=> 110
total(1, 10)
=> 55
動きはわかったけど、で、実際の現場ではどんな場面でyieldを使えばいいの?
yieldの使いどころ
yield(ブロック)は使い方によっては色々できますが、現場では以下のケースでよく利用されます。
– メソッドを利用者が柔軟に拡張出来るようにする(処理の一部差し替えを行う)
– 処理の一部を利用者に委ねる
それぞれ具体例を交えて解説します。
メソッドを利用者が柔軟に拡張出来るようにする
例えば、デフォルトだと、渡された2つの和を計算するメソッドだけど、ブロックを渡すことで、独自の計算をすることができるcalculationメソッドは下のようになります。
# ブロックが渡されていればその計算式を実行し、ブロックがなければ足算を行う
def calculation(first, second)
block_given? ? yield(first, second) : first + second
end
calculation(10, 20)
=> 30
calculation(10, 20) {|first, second| first * second }
=> 200
calculation(10, 20) {|first, second| second / first }
=> 2
「キッチンと具材と最低限の調味料は準備したので、あとは好きなように料理を作ってくれ。必要があれば追加の具材や調味料を加えてもOK」という感じです。
処理の一部を利用者に委ねる
例えば、スクレイピングをする際には画面遷移毎に「描画の完了待ち」→「なんらかの処理」という流れで処理が行われます。
def login_page
get 'https://engineer-negoto.com/' # URLにアクセス
wait_loading # 画面描画の完了まち
check_error_page # 画面読み込み後、エラー画面が表示されていた場合のハンドリング
# ID, PWを入力しログインボタンをクリック
input_id
input_password
click_button('login')
end
def top_page
wait_loading # 画面描画の完了まち
check_error_page # 画面読み込み後、エラー画面が表示されていた場合のハンドリング
# 詳細画面へ遷移できるのボタンをクリックする
click_button('detail')
end
def detail_page
wait_loading # 画面描画の完了まち
check_error_page # 画面読み込み後、エラー画面が表示されていた場合のハンドリング
download_html # HTMLをダウンロードする
end
コードには画面の描画待ちとエラーチェックが重複して冗長なコードとなっています。
これは下のように置き換えることができ、コードの重複がなくなり、拡張性を持たせることができます。
def process_page(url = nil)
get url if url # URLにアクセス
wait_loading # 画面描画の完了まち
check_error_page # 画面読み込み後、エラー画面が表示されていた場合のハンドリング
yield if block_given?
end
def login_page
process_page('https://engineer-negoto.com/') do
# ID, PWを入力しログインボタンをクリック
input_id
input_password
click_button('login')
end
end
def top_page
# 詳細画面へ遷移できるのボタンをクリックする
process_page { click_button('detail') }
end
def detail_page
# HTMLをダウンロードする
process_page { download_html }
end
まとめ
- yieldはメソッドに渡されたブロックを実行するキーワード
yieldの使い方と使いどころが理解できたかと思います。
Rubyのブロックはロジックに汎用性を持たすことができるため、是非理解して使いこなしてみてください。