yossy-dev

I’m Software Enginner.

block・yield・Procについて(Ruby)

はじめに

yield・Procを見るたび調べて思い出すということを繰り返してたのでまとめておく。

まとめのために「プロを目指す人のためのRuby入門」の10章を読み直したのでその備忘録。


ブロックを使うメソッドの定義とyieldの挙動

yield はメソッド呼び出し時に紐付けられたブロックを実行する。

例)

def greeting
  puts 'おはよう'
  # ここでブロックの処理を呼び出す
  yield
  puts 'こんばんは'
end

# ブロック付きでメソッドを呼び出す
greeting do
  puts 'こんにちは'
end
# => おはよう
# こんにちは
# こんばんは

# ブロックなしでメソッドを呼び出すとyieldでブロックを呼び出そうとした際にエラーになる
greeting
# => おはよう
#    LocalJumpError: no block given (yield)

# --- エラーにしたくない場合 --- #
# block_given?でブロック付きで呼び出されたか確認できる
def greeting
  puts 'おはよう'
  # ブロックが渡されたかどうかを確認→渡されている場合true
  if block_given?
    yield
  end
  puts 'こんばんは'
end

greeting do
  puts 'こんにちは'
end
# => おはよう
# こんにちは
# こんばんは

greeting
# => おはよう
#    こんばんは

参考:プロを目指す人のためのRuby入門 第10章

yieldはブロックに引数を渡したり、ブロックの戻り値を受けとったりできる。

def greeting
  puts 'おはよう'
  # ブロックに引数を渡し、戻り値を受け取る
  text = yield 'こんにちは'
  puts text
  puts 'こんばんは'
end

greeting do |text|
  text * 2
end
# => おはよう
# こんにちはこんにちは
# こんばんは

参考:プロを目指す人のためのRuby入門 第10章

ブロックはメソッドの引数として明示的に受け取ることもできる。

ブロックを引数として受け取る場合は、引数名の前に & をつける、またそのブロックを実行する場合は call メソッドを使って実行する。

後述するが引数のブロックはProcオブジェクト(なのでcallメソッドで呼び出せる)。

def メソッド(&引数)
  # ブロックを実行する
  引数.call
end

def greeting(&block)
  puts 'おはよう'
  # ブロックが渡されていなければblockはnil
  unless block.nil?
    text = block.call('こんにちは')
    puts text
  end
end

greeting
# => おはよう

greeting do |text|
  text * 2
end
# => おはよう
#    こんにちはこんにちは

参考:プロを目指す人のためのRuby入門 第10章

Procオブジェクトについて

Procクラスはブロックをオブジェクト化するためのクラス。

procedure = 手続き、手順

Procクラスはブロック(何らかの処理)を表す。

Procクラスのインスタンス(Procオブジェクト)を作成する場合は、Proc.newにブロックを渡す

# --- Procについて --- #

# Hello!という文字列を返すProcオブジェクトを作成する。
hello_proc = Proc.new do
  'Hello!'
end

# {}でも可
hello_proc Proc.new { 'Hello!' }

# Procオブジェクトを実行したい場合はcallメソッドを使う
hello_proc Proc.new { 'Hello!' }
hello_proc.call # => "Hello!"

# 実行時に引数を利用するProcオブジェクトも定義できる。
add_proc = Proc.new { |a, b| a + b }
add_proc.call(10, 20) # => 30

# 引数にデフォルト値をつけることもできる(デフォルト値だけでなく、可変長引数やキーワード引数なども普通のメソッドと同じように受け取れる)
add_proc = Proc.new { |a = 0, b = 0| a + b }
add_proc.call # => 0
add_proc.call(10) # => 10
add_proc.call(10, 20) # => 30

# Proc.newのかわりにprocメソッドを使うこともできる
add_proc = proc { |a, b| a + b}

# --- ブロックの代わりにProcオブジェクトをメソッドの引数として渡すこともできる --- #

# メソッドが受け取れるブロックの数は1つまで
def greeting(&block)
  puts 'おはよう'
  text = block.call('こんにちは')
  puts text
end

repeat_proc = Proc.new { |text| text * 2 }
# Procオブジェクトをブロックの代わりに渡す際は&を付けて渡す。(ないとブロックではなく普通の引数とみなされる)
greeting(&repeat_proc)
# => おはよう
#    こんにちはこんにちは

# --- Procオブジェクトを普通の引数として受け取って実行する --- #

# Procオブジェクトはただのオブジェクトなので引数として渡す分には制限がない
def greeting(original_proc)
  puts 'おはよう'
  text = original_proc.call('こんにちは')
  puts text
end

repeat_proc = Proc.new { |text| text * 2 }
# 普通の引数としてProcオブジェクトを渡す
greeting(repeat_proc)
# => おはよう
#    こんにちはこんにちは

# --- lambdaでProcオブジェクトを作成する --- #

# 以下の方法でもProcオブジェクトを作成できる
->(a, b) { a + b }
lambda { |a, b| a + b}

参考:プロを目指す人のためのRuby入門 第10章

&とto_proc

Procオブジェクトをブロックとして渡したい場合は引数の前に&をつける必要がある

reverse_proc = Proc.new { |s| s.reverse }
# mapメソッドにブロックを渡すかわりにProcオブジェクトを渡す(&が必要)
['Ruby', 'Go', 'Rust'].map(&reverse_proc) # => ['ybuR', 'oG', 'tsuR']

&の役割

  • Procオブジェクトをブロックとして認識させる
  • 右辺(上の例だとreverse_proc)のオブジェクトに対して, to_proc メソッドを呼び出して戻り値として得たProcオブジェクトをブロックを利用するメソッドに与える
    • → ただし元からProcオブジェクトだった場合はto_procメソッドを呼んでも自分自身が返るだけ。

シンボルは to_proc メソッドを持っている。

シンボルを変換してできたProcオブジェクトが変わっている点

  • シンボル自身はレシーバに対して呼び出すメソッドの名前になる
  • 第1引数がシンボルで指定したメソッドのレシーバになる
  • 第2引数以降はシンボルで指定したメソッドの引数になる

例)

# シンボル→Procオブジェクトに変換したときの挙動
split_proc = :split.to_proc
split_proc # => #<Proc:0x00007fd43a890df8(&:split)>

# 引数が一つの場合、'a-b-c-d e'.splitと同じ
split_proc.call('a-b-c-d e') # => ["a-b-c-d", "e"]

# 引数が複数の場合、'a-b-c-d e'.split('_', 3)のように
# 第2引数以降はシンボルで指定したメソッドの引数になる
split_proc.call('a-b-c-d e', '-', 3) # => ["a", "b", "c-d e"]

参考:プロを目指す人のためのRuby入門

map(&:upcase)のような書き方の挙動

['ruby', 'go', 'rust'].map { |s| s.upcase } # => ["RUBY", "GO", "RUST"]
['ruby', 'go', 'rust'].map(&:upcase) # => ["RUBY", "GO", "RUST"]

上記からこの書き方の挙動が理解できる

  1. &:upcaseはシンボルの:upcaseに対してto_procオブジェクトを呼び出す
    • :upcase.to_proc
  2. シンボルの:upcaseがProcオブジェクトに変換され、mapメソッドにブロックとして渡される(こんな感じ)
  3. 上記②で作ったProcオブジェクトはmapメソッドから配列の各要素を実行時の第一引数として受け取る。
  4. 第一引数はupcaseメソッドのレシーバとなる。→ つまり配列の各要素に対してupcaseメソッドを呼び出す。

    ruby # 2でブロックとしてmapに渡すので、2 ~ 4は単にこれをやってる ['ruby', 'go', 'rust'].map do |block_arg| block_arg.upcase end

  5. 配列の各要素が大文字に変換された新しい配列がmapメソッドの戻り値になる

おわりに

まとめ

  • ブロックは手続き(一連の処理)のまとまり
  • ブロックやProcオブジェクトを渡せるようなメソッドを定義すると処理の一部に対して外部からカスタマイズすることができる振る舞いを組み込める

map(&:upcase)みたいな書き方なんとなくしてたけどどういう動きで処理を実行できてるのかちゃんと理解できてよかった。

時々yieldとか見るとなんだっけ、、ってなってしまってたので今後に活きるといいな。

参考資料

  • プロを目指す人のためのRuby入門