【Rubyブロック】yieldとProcオブジェクト

Rubyの文法の中で、
やはり避けては通れないぐらい重要な概念として、
ブロックがあります。

このブロックは、ものすごくよく使いますし、
重要な概念ですが、
その奥深いところは、結構難しいかなと思います。

本記事は、Ruby基礎編と言っていながら、
その点やや応用編になるかと思います。

ブロックについて、詳しく知らなくてもいい。
とりあえず、どんな感じなのか、ざっと理解できれば十分という方は、
こちらの記事を御覧ください。

本記事では、ブロックについて、
詳細に解説します。

また、下記の動画でRuby全般の文法について、
一通り解説しているので、よろしければご参考ください。

ブロックとは?

ブロックの概要については、
既に下記の記事で解説していますが、

ブロックは、簡単に言えば、
do~endとか{}とかの塊を指しており、
処理の塊みたいなものです。

処理のかたまりといえば、
関数とかが想起されますが、
Rubyにはすべてがオブジェクトという思想があるため、
この処理のかたまり(すなわちブロック)もオブジェクトとして語られます。
(後に解説するProcオブジェクト)

本記事では、こうしたRubyのブロック周りについて触れていきますが、
まず、yieldというメソッドについて言及します。

yield

Rubyのメソッドには、
引数としてブロックを渡すことができます。

試しにメソッド呼び出しを、
ブロック付きで呼び出してみるとします。

def say
  puts 'hello world'
  yield
end

say do
  puts 'ブロックで呼ばれました'
end

say do ~ endの部分が、
sayメソッドの呼び出しで、
引数にdo ~ endのブロックを渡していますね。

さて、ここで、sayメソッドの方に、
yieldという記法が出現していますが、
これはブロックを実行するメソッドです。

上記の場合、say doという感じで、
ブロックとしてメソッドが実行されているので、
メソッドの内部でyieldとされた場合、ブロック内の、
puts 'ブロックが呼ばれました'が実行されます。

そのため、ブロックなしでメソッドを呼んでいて、
yieldが使われているとエラーになります。

def say
  puts 'hello world'
  yield
end

say #  `say': no block given (yield) (LocalJumpError)となる

以上のように、
yieldは、ブロックを呼ぶときに
使うメソッドですね。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#yield

なので、ブロックが渡されているのかどうかを
メソッド内で条件分岐したい場合は、block_given?を使うといいかもしれません。

def say
  puts 'hello world'

  if block_given?
    yield
  end

  puts 'hello2 world'
end

say do
  puts 'ブロックが呼ばれました'
end

say # メソッド内でblock_given?で分岐しているのでエラーにならない

また、yieldに引数を渡すこともできます。

def say
  initial_number = yield 1111 # ①
  puts initial_number # 2222と返す ④
end

say do |value|
  puts value # 1111と返す ②
  value = value * 2 # ③
end

ややこしいですが、
まず最初の二行目のyieldでブロック先に飛びます。

コメントで①とか番号振ってありますが、
その順番で処理が実行されています。

最初はyieldの引数の1111がそのままputsされ、
valueの中身を二倍します。

これで引数の中身が書き換わったので、
initial_numberの中身が2222となって出力されています。

ブロックを引数として明示的に受け取る

ブロックを引数として明示的に受け取ることもでき、
その場合、&という記法を使います。

この明示的というキーワードですが、
さっきのコードも、
do ~ endがメソッドの引数とみなせますよね。

ただ、上記の場合、明示的とは言いにくいと思います。

これに対して、明示的にブロックが引数であることを
強調した書き方が&という記法にあたります。

呼び出すときにはcallで記載しています。

def say(&args)
  initial_number = args.call 1111 # ①
  puts initial_number # 2222と返す ④
end

say do |value|
  puts value # 1111と返す ②
  value = value * 2 # ③
end

Procクラス

RubyにはProcクラスというものがあり、
これはブロックをオブジェクト化したものです。

https://docs.ruby-lang.org/ja/latest/class/Proc.html

value = Proc.new { 'hello world' }
puts value.call

Proc.newという形で作成でき、
中身のブロックを呼び出すときは、
callを使用します。

Procと似たようなものとして、
procメソッドやラムダというものがあります。

# value = Proc.new { 'hello world' }
value0 = proc{ 'hello' }
# ラムダ
value1 = -> { 'hello world' } # 引数がある場合 "-> (a, b) { a * b }"のようにする。(a, b)の()は省略可
value2 = lambda { |a| a * 2 }
puts value0.call
puts value1.call
puts value2.call(10)

どれも挙動としては似ているのですが、
ラムダとProc.newの違いとして有名なのが、
引数の扱いです。

# Proc.newの場合
value = Proc.new { |a, b, c| a.to_i + b.to_i + c.to_i } # nil.to_iを0にするため
puts value.call(1, 2) # 引数が1つ足りない
puts value.call(1, 2, 3, 4, 5) # 引数が2つ多い
# 上記2つは、呼び出せる

# ラムダの場合
value1 = -> a, b, c { a.to_i + b.to_i + c.to_i }
# puts value1.call(1, 2) # wrong number of arguments (given 2, expected 3)
puts value1.call(1, 2, 3) # 正常
# puts value1.call(1, 2, 3, 4, 5) # wrong number of arguments (given 5, expected 3)

# 引数の数が合わないとArgumentErrorが起きる

これ以外にも、ジャンプ構文内での挙動の違いなどあるのですが、
細かいので引数の扱いが異なるっていうことだけおさえておけば
十分かなと思います。

Macとマグカップの画像

&:upcaseの本当の意味

以前の記事で、&:メソッドの記法について、
少し解説しましたが、どういう仕組みでこうなっているかは触れませんでした。

[st-card myclass="st-no-shadow" id="724" label="参考" pc_height="" name="" bgcolor="#cccccc" color="#ffffff" webicon="st-svg-file-text-o" readmore="on" thumbnail="on" type="text"]

ここでこの記法の意味について解説します。

まず、この記法のみそは、to_procメソッドにあります。

https://docs.ruby-lang.org/ja/latest/method/Symbol/i/to_proc.html

https://docs.ruby-lang.org/ja/latest/method/Method/i/to_proc.html

value = Proc.new { |a, b| a + b }
puts value.to_proc # #<Proc:0x00007faf9b91f210 index3.rb:1>みたいに返る

上記のようにprocオブジェクトを返すメソッドです。

これがシンボルの場合、少し変わった挙動をします。

value = :to_s.to_proc
puts value # #<Proc:0x00007f796288b4a8(&:to_s)>
ans = value.call(1)
puts ans.class # 1.to_s.classと同じ挙動

上記は、to_procでシンボルをProcオブジェクトにして、
それに対して、引数を一つ渡してcallしています。

その際、1つ目の引数をレシーバーにして、
元のシンボル名と同名のメソッドを実行します。

すなわち、1つ目の引数、1に対して、
元のシンボル名、to_sと同名のto_sメソッドを実行するので、
1.to_sと同様の挙動になるということです。

さて、ここで下記のコードをもう一度見てみましょう。

[1, 2, 3, 4].map(&:to_s)

意味としては、1とかの数字をstringにするという感じですが、
どういう仕組でこうなるのかというと、

[st-flexbox url="" rel="nofollow" target="" webicon="" title="実行順序" width="" height="" color="#fff" fontsize="200" radius="0" shadow="#424242" bordercolor="#ccc" borderwidth="1" bgcolor="#ccc" backgroud_image="" blur="on" left="" margin_bottom="0" myclass=""]

  1. &はブロックを引数として受け取るので、:to_sというシンボルをブロック引数として受け取る
  2. &でto_procメソッドが呼ばれて、:to_sシンボルはProcオブジェクトになる
  3. mapによって、1とか2のような数字が第一引数としてProcオブジェクトに渡される。
  4. 引数(この場合1とか2)に対して、元のシンボル名と同じto_sメソッドが実行される
  5. mapによって実行されたものが新しい配列に格納される

という流れによって、
[1, 2, 3, 4]の配列が、すべてStringにした
新しい配列が作成されます。

ブロックの活用例

試しにブロックを使って、
ちょっと複雑なコードを書いてみます。

def calc(value, &block)
  if block_given?
    value.each(&block)
  else
    value.each do |f|
      puts f * 2
    end
  end
end

kakeru_3 = Proc.new { |a| puts a * 3 }
kakeru_2_tasu_3 = Proc.new { |a| puts a * 2 + 3 }

calc([10, 20, 30], &kakeru_3)
calc([10, 20, 30], &kakeru_2_tasu_3)
calc([10, 20, 30])

このように、ブロック引数に対して、eachを呼び出すとかすれば、
メソッドの汎用性を高められるでしょう。
(railsのコードとかでは、こういう記法がバシバシ使われている気がする)

つまり、メソッドを呼び出したら画一的に似たようなブロックが実行されるのではなく、
メソッド呼び出し時に、どういうブロックを実行させるかを選択させるようなイメージです。

正直、こういうコードを書くのはめちゃくちゃ難易度が高いと思います。
(上記のコードはただのサンプルで汚いのはお許しを、
あくまでイメージを共有したかっただけですので)

自由自在に、こうしたブロック引数に処理をさせたりして、
結合度が弱い汎用性が高いコードを書けるように慣れれば、
もうかなりのRails上級者といえるでしょう。