んなまにのメモ帳

気が向いたときに更新されます。

Ruby ブロックを受け取るメソッドを作る方法

ブロックdo 〜 end{|hoge| hoge.bar }を受け取って何かするメソッドを自分で作る方法。

%w(hoge foo bar).each do |s|
  puts s
end

のようなメソッドを自分で作りたくなる時は意外と多いので、覚えられるようにメモしておこうと思います。

自前のメソッドでブロックを実行する方法

主に、以下2通りがあるようです。

  • ブロック引数&blockを使う方法
  • yieldを使う方法

ブロック引数を使う方法

メソッドの引数の一番後ろに&で始まる仮引数を定義すると、これでブロックを受け取ることができます。 &がついているとブロックをProcオブジェクトに展開されるので、block.callでブロックを実行することができます。 Procオブジェクトについては、まだよくわかってないので、今度調べたいと思います。

def execute_block(&block)
  block.call
end

execute_block do
  puts 'Hello'
end

# ↓出力
# Hello
# => nil

ブロックに対して引数を渡すには、callメソッドに引数を渡します。

def execute_block_with_args(&block)
  a = 'Hello'
  b = 'world'

  block.call(a, b)
end

execute_block_with_args do |hello, world|
  puts "#{hello}, Ruby #{world}!"
end

# ↓出力
# Hello, Ruby world!
# => nil

callに渡した値が、ブロックの引数||に囲われた変数に渡されます。

yieldを使う方法

yieldを使うと、ブロック変数の宣言の省略ができる。

def execute_block
  yield('Hello', 'world')
end

execute_block do |a, b|
  puts "#{a}, Ruby #{b}!"
end

# ↓出力
# Hello, Ruby world!
# => nil

ブロックは渡されたか?

yeildを使う場合、&blockブロック引数を省略できるため、メソッド自体がブロックが渡されたかどうかは、メソッドの中からはわかりません。

そのためかどうかは知りませんが、block_given?というメソッドが用意されています。 block_given?は、ブロックが渡された場合にtrueになるため、メソッド内でブロックの有無によって分岐させることができます。

def give_hello_if_block_given
  if block_given?
    yield 'hello'
  else
    puts 'Please give me block!'
  end
end

give_hello_if_block_given do |a|
  puts "Block was given #{a}."
end

# ↓出力
# Block was given hello.
# => nil

give_hello_if_block_given

# ↓出力
# Please give me block!
# => nil

ブロックを渡すメリット

自作のメソッドでブロックを渡せると何が嬉しいかというと、あるオブジェクトを開いて使い終わったら必ずリソースを解放したいみたいなときに便利だと思います。

以下に、とりあえず何かを開いた状態か開いていない状態かを管理するクラスSomethingを作りました。

Somethingは、new直後に@openedfalseで初期化します。何も開いていない状態です。 また、openメソッドとcloseメソッドを持っており、@openedの状態を書き換えることができ、opened?で今の状態を問い合わせることができます。

class Something
  def initialize
    @opened = false
  end

  def open
    @opened = true
    puts 'Opened something'
  end

  def close
    @opened = false
    puts 'Closed something'
  end

  def opened?
    @opened
  end
end

これを操作して、特定のスコープの範囲内でのみ、Somethingを開き、範囲外になったらSomethingを閉じるような使い方をしたいと思います。

普通にやると以下のように、open何かやるcloseですが、これはクローズのし忘れなどを招きやすくなります。

something = Something.new

p something.opened?
# ↓出力
# false
# => false

something.open
# ↓出力
# Opened something
# => nil

p something.opened?
# ↓出力
# true
# => true

something.close
# ↓出力
# Closed something
# => nil

p something.opened?
# ↓出力
# false
# => false

以下のようにブロックを受け取るメソッドを作ると、そのブロックの範囲内でオブジェクトを開くことができるようになります。

def with_open_something(something)
  # メソッド開始時にオープンする処理をやる
  something.open

  if block_given?
    yield something
  end

ensure
  # メソッドが終わる前に、とりあえずクローズする
  something.close
end

上で

something = Something.new

p something.opened?
# ↓出力
# false
# => false

with_open_something(something) do |s|
  p something.opened?
end
# ↓出力
# Opened something
# true
# Closed something

p something.opened?
# ↓出力
# false
# => false

with_open_somethingを実行した際に、openメソッドの実行に続けて、ブロック内の処理が実行され、その後closeするという動きになっていることがわかります。

まとめ

ブロックの使い方を実行しながら確認してみました。 Procオブジェクトが密接に絡んでくるようなので、Procを理解できるともっと色々できるかもしれません。今度調べてみたいと思います。 オープンしてクローズするパターンを試してみましたが、ある処理の前後の処理を共通化したいみたいなときにも使えて便利だと思います。