Ruby学習 「プロを目指す人のためのRuby入門」 10章

はじめに

プロを目指す人のためのRuby入門Rubyを学習しています。
大事そうな部分や理解が難しかった部分、忘れそうなことについて学習メモを書きます。
今回は第10章「yieldとProcを理解する」について書いていきます。

記事中のプログラムはirbで動作を確認しました。

ブロックを利用するメソッドの定義とyield

  • メソッドの呼び出しにブロックをつけて、メソッド内でyieldを使うと、yieldの部分でブロックの処理が実行される
def greeting
  puts 'おはよう'
  yield
  puts 'こんばんは'
end

greeting do
  puts 'こんにちは'
end
おはよう
こんにちは
こんばんは
=> nil
  • yieldを複数回書くと、ブロックの処理も複数回実行される
  • ブロックを渡さずに、yieldを使用するとエラーになる
  • ブロックが渡されたか確認する場合はblock_given?メソッドを使用する
  • yieldはブロック人引数を渡したり、ブロックの戻り値を受け取ったりできる
  • yieldで渡す引数がブロック引数より多かった場合は無視され、少なかった場合はnilが設定される
  • ブロックを引数として明示的に受け取る場合は引数名の先頭に&をつけ、ブロックの処理を実行する際はcallメソッドを使用する
def greeting(&block)
  puts 'こんにちは'
  name = block.call('さん')
  puts name
  puts '御用は何でしょうか'
end
=> :greeting

greeting do |honorific|
  '田中' + honorific
end
こんにちは
田中さん
御用は何でしょうか
=> nil
  • メソッドにブロック引数を設定した際に、ブロックが渡されたかどうかはブロック引数がnilかどうかで判断する
  • ブロック引数は1メソッドにつき1つしか指定できない
  • メソッドにブロック引数を設定した場合でもyieldやblock_given?メソッドを使用可能
  • ブロックを引数として受け取ることで、ほかのメソッドにブロックを渡すことができる
  • ブロックをほかのメソッドに渡す場合は、引数の頭に&をつける(&をつけないと普通の引数としてみなされる)
  • ブロック引数.arityでブロック引数の個数を確認できる

Procオブジェクト

  • Procクラスはブロックをオブジェクト化するクラス
  • Procオブジェクトはcallメソッドで実行する
test_proc = Proc.new do
  'Test'
end
=> #<Proc:0x00000000be7260@(irb):11>

test_proc = Proc.new { 'Test' }
=> #<Proc:0x00000000bf6328@(irb):15>

test_proc.call
=> "Test"
  • 引数を利用するProcオブジェクトも作成できる
concat_proc = Proc.new { |a, b| a + b }
=> #<Proc:0x00000001df26a8@(irb):1>

concat_proc.call('abc', 'efg')
=> "abcefg"
  • 引数にはデフォルト値や可変長引数等、メソッドと同じように引数を設定できる
  • Procオブジェクトを作成する場合は、Kernelモジュールのprocメソッドを使用することもできる
concat_proc = proc { |a, b| a + b }
=> #<Proc:0x000000012f5bd8@(irb):1>

concat_proc.call('abc', 'defg')
=> "abcdefg"
  • Procオブジェクトをメソッドに渡す場合は、引数に&をつける必要がなく、引き渡す個数の制限もない
  • Procオブジェクトは下記の方法でも作成できる(これをラムダと呼ぶ)
# 引数リストの()は省略可能
# do~endも使用可能。
concat_proc = ->(a, b) { a + b }
=> #<Proc:0x0000000118c968@(irb):3 (lambda)>

concat_proc.call('abc', 'def')
=> "abcdef"

concat_proc = lambda { |a, b| a + b }
=> #<Proc:0x00000001170880@(irb):5 (lambda)>

concat_proc.call('ghi', 'jklmn')
=> "ghijklmn"
  • ラムダは引数の数に過不足があるとエラーになる
  • ProcクラスのインスタンスがProc.newで作られたか、ラムダとして作られたか判断するにはlambda?メソッドを使用する

Procオブジェクトについてもっと詳しく

  • Procオブジェクトの実行方法はcallメソッドを使用する以外にも複数ある
concat_proc = ->(a, b) { a + b }

concat_proc.call('ab', 'cde')
=> "abcde"
concat_proc.yield('fg', 'h')
=> "fgh"
concat_proc.('ijk', 'lmno')
=> "ijklmno"
concat_proc['pq', 'rs']
=> "pqrs"

hello_proc = ->(name) { "hello #{name}" }
=> #<Proc:0x00000001369ee8@(irb):1 (lambda)>

hello_proc === 'tanaka'
=> "hello tanaka"
  • Procオブジェクトが===で呼び出せるようになっているのは、case文のwhen節でProcオブジェクトを使えるようにするため
  • ブロックを引数として渡す場合につける&は、右辺のオブジェクトにto_procメソッドを呼び出して得られたProcオブジェクトをメソッドに渡す役割がある
  • シンボルもto_procメソッドを持っている
  • シンボルから作成したProcオブジェクトに引数を1つ渡して実行すると、その引数をレシーバとして元のシンボルと同じ名前のメソッドを呼び出す
  • シンボルから作成したProcオブジェクトに引数を複数渡すと2つ目以降はシンボルで指定したメソッドの引数になる
upcase_proc = :upcase.to_proc
=> #<Proc:0x00000002410378(&:upcase)>

upcase_proc.call('abc')
=> "ABC"

concat_proc = :concat.to_proc
=> #<Proc:0x0000000259b4b8(&:concat)>

concat_proc.call('abc', 'def')
=> "abcdef"

*メソッドの引数やローカル変数をProcオブジェクト内で参照すると、メソッドの実行が終了してもProcオブジェクトは引き続き引数やローカル変数にアクセスできる * 上記のようなメソッドをクロージャ(関数閉方)という

def generate_proc(array) 
  sum= 0
  Proc.new do |num|
    sum+= num
    array << sum
  end
end
=> :generate_proc

values = []
=> []
sample_proc = generate_proc(values)
=> #<Proc:0x000000024932a0@(irb):3>
values
=> []

sample_proc.call(5)
=> [5]
values
=> [5]

sample_proc.call(10)
=> [5, 15]
values
=> [5, 15]
  • Proc.newではreturnを実行するとメソッドを抜ける
  • ラムダでreturnを実行するとラムダ内の処理を抜ける
  • Proc.newでbreakを実行すると例外が発生する
  • ラムダでbreakを実行しても例外は発生せず、ループ処理は最後まで実行される

検討事項

Procオブジェクトが===で呼び出せるようになっている理由は何か?

high = Proc.new { |n| n > 180 }
low = Proc.new { |n| n < 160}
height= 80

case height
when high
  '高い'
when low
  '低い'
else
  '普通'
end

上記のようなプログラムを実行すると、内部的には「high === height」や「low === height」という処理が行われる。そのためwhen節でブロックの処理を実行できるようにするために===でもブロック処理できるように、Procクラスで===を再定義している。

感想

Procについて学び、共通の処理にしたいが一部だけ処理を変えたいとき等に有効だということが分かった。柔軟性の高いプログラムを作成するために活用できるようにする。

参考文献

この記事は以下の情報を参考にして執筆しました。