💎

【Ruby 33日目】基本文法 - ブロック引数

に公開

はじめに

Rubyのブロック引数について、Ruby 3.4の仕様に基づいて詳しく解説します。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

ブロック引数は、メソッドにブロックを渡すための機能です:

  • &パラメータ - ブロックを明示的にProcオブジェクトとして受け取る
  • block_given? - ブロックが渡されているかを確認
  • yield - ブロックを実行(暗黙的な呼び出し)
  • call - Procオブジェクトとして明示的に呼び出し

ブロック引数を使うことで、メソッドの振る舞いを呼び出し側でカスタマイズできます。

基本的な使い方

基本的なブロック引数

def execute(&block)
  puts "Before block"
  block.call
  puts "After block"
end

execute { puts "Inside block" }
#=> Before block
#   Inside block
#   After block

ブロックに引数を渡す

def repeat(n, &block)
  n.times do |i|
    block.call(i)
  end
end

repeat(3) { |num| puts "Iteration: #{num}" }
#=> Iteration: 0
#   Iteration: 1
#   Iteration: 2

ブロックの有無を確認

def greet(name, &block)
  greeting = "Hello, #{name}!"

  if block_given?
    block.call(greeting)
  else
    puts greeting
  end
end

greet("Alice")
#=> Hello, Alice!

greet("Bob") { |msg| puts msg.upcase }
#=> HELLO, BOB!

ブロックをProcとして保存

class Task
  def initialize(&block)
    @block = block
  end

  def execute
    puts "Executing task..."
    @block.call if @block
  end
end

task = Task.new { puts "Task completed!" }
task.execute
#=> Executing task...
#   Task completed!

複数のブロック引数

def transform(&block)
  data = [1, 2, 3, 4, 5]
  data.map(&block)
end

result = transform { |n| n * 2 }
puts result.inspect  #=> [2, 4, 6, 8, 10]

yieldとブロック引数の併用

def execute_with_timing(&block)
  start_time = Time.now
  result = yield  # yieldを使用
  end_time = Time.now

  puts "Execution time: #{end_time - start_time} seconds"
  result
end

execute_with_timing { sleep(0.1); "Done" }
#=> Execution time: 0.100... seconds

よくあるユースケース

ケース1: リソース管理

ブロック引数を使用してファイルやデータベース接続を安全に管理します。

class DatabaseConnection
  def self.open(database, &block)
    connection = new(database)
    puts "Opening connection to #{database}"

    begin
      block.call(connection)
    ensure
      connection.close
    end
  end

  def initialize(database)
    @database = database
    @connected = true
  end

  def query(sql)
    "Executing: #{sql}"
  end

  def close
    puts "Closing connection to #{@database}"
    @connected = false
  end
end

DatabaseConnection.open("mydb") do |db|
  puts db.query("SELECT * FROM users")
end
#=> Opening connection to mydb
#   Executing: SELECT * FROM users
#   Closing connection to mydb

ケース2: トランザクション処理

ブロック内でトランザクションを自動的に管理します。

class Transaction
  def self.execute(&block)
    transaction = new
    transaction.begin

    begin
      result = block.call(transaction)
      transaction.commit
      result
    rescue => e
      transaction.rollback
      puts "Error: #{e.message}"
      nil
    end
  end

  def begin
    puts "BEGIN TRANSACTION"
  end

  def commit
    puts "COMMIT"
  end

  def rollback
    puts "ROLLBACK"
  end

  def save(data)
    puts "Saving: #{data}"
  end
end

# 成功ケース
Transaction.execute do |txn|
  txn.save("User 1")
  txn.save("User 2")
  "Success"
end
#=> BEGIN TRANSACTION
#   Saving: User 1
#   Saving: User 2
#   COMMIT

# 失敗ケース
Transaction.execute do |txn|
  txn.save("User 1")
  raise "Something went wrong"
  txn.save("User 2")  # 実行されない
end
#=> BEGIN TRANSACTION
#   Saving: User 1
#   ROLLBACK
#   Error: Something went wrong

ケース3: コールバック登録

イベント処理やフック機能を実装します。

class EventEmitter
  def initialize
    @listeners = Hash.new { |h, k| h[k] = [] }
  end

  def on(event, &block)
    @listeners[event] << block
  end

  def emit(event, *args)
    @listeners[event].each do |listener|
      listener.call(*args)
    end
  end
end

emitter = EventEmitter.new

emitter.on(:user_created) do |user|
  puts "Welcome email sent to #{user[:email]}"
end

emitter.on(:user_created) do |user|
  puts "User #{user[:name]} added to analytics"
end

emitter.emit(:user_created, { name: "Alice", email: "alice@example.com" })
#=> Welcome email sent to alice@example.com
#   User Alice added to analytics

ケース4: ビルダーパターン

DSL風のインターフェースを提供します。

class HTMLBuilder
  def initialize
    @content = []
  end

  def div(&block)
    @content << "<div>"
    yield if block_given?
    @content << "</div>"
  end

  def p(text)
    @content << "<p>#{text}</p>"
  end

  def h1(text)
    @content << "<h1>#{text}</h1>"
  end

  def build(&block)
    instance_eval(&block) if block_given?
    @content.join("\n")
  end
end

html = HTMLBuilder.new.build do
  h1 "Welcome"
  div do
    p "This is a paragraph"
    p "This is another paragraph"
  end
end

puts html
#=> <h1>Welcome</h1>
#   <div>
#   <p>This is a paragraph</p>
#   <p>This is another paragraph</p>
#   </div>

ケース5: 遅延評価とメモ化

計算を遅延させ、必要に応じて実行します。

class LazyValue
  def initialize(&block)
    @block = block
    @cached = false
    @value = nil
  end

  def value
    unless @cached
      puts "Computing value..."
      @value = @block.call
      @cached = true
    end
    @value
  end

  def reset
    @cached = false
    @value = nil
  end
end

expensive_calculation = LazyValue.new do
  sleep(0.1)  # 重い計算をシミュレート
  42
end

puts "Created lazy value"
#=> Created lazy value

puts expensive_calculation.value
#=> Computing value...
#   42

puts expensive_calculation.value  # キャッシュされた値を返す
#=> 42

expensive_calculation.reset
puts expensive_calculation.value  # 再計算
#=> Computing value...
#   42

注意点とベストプラクティス

注意点

  1. ブロック引数は最後に配置
# BAD: ブロック引数の後に通常引数は置けない
# def bad_example(&block, arg)  # SyntaxError
# end

# GOOD: ブロック引数は最後
def good_example(arg1, arg2, &block)
  block.call(arg1, arg2) if block
end

good_example(1, 2) { |a, b| puts a + b }  #=> 3
  1. &の使い分け
# メソッド定義時の&:ブロックをProcとして受け取る
def method_with_block(&block)
  block.call
end

# メソッド呼び出し時の&:ProcをブロックとしてEND渡す
my_proc = proc { puts "Hello" }
method_with_block(&my_proc)  #=> Hello

# Symbol#to_procを利用
numbers = [1, 2, 3]
puts numbers.map(&:to_s).inspect  #=> ["1", "2", "3"]
  1. yieldとcallのパフォーマンス
# yieldの方が高速(Procオブジェクトを作らない)
def with_yield
  yield
end

# callの方が柔軟(Procとして保存・渡せる)
def with_call(&block)
  block.call
end

# パフォーマンスが重要な場合はyieldを使う
def fast_iteration(n)
  n.times { |i| yield(i) }
end

fast_iteration(3) { |i| puts i }
#=> 0
#   1
#   2

ベストプラクティス

  1. ブロックの有無を明確にする
# GOOD: ブロックが必須であることを明示
def requires_block
  raise ArgumentError, "Block required" unless block_given?
  yield
end

# GOOD: ブロックがオプションであることを明示
def optional_block(&block)
  if block
    block.call
  else
    "No block provided"
  end
end

puts optional_block  #=> No block provided
puts optional_block { "With block" }  #=> With block
  1. ブロック引数に意味のある名前を付ける
# GOOD: ブロックの用途が明確
def with_transaction(&transaction_block)
  begin_transaction
  result = transaction_block.call
  commit_transaction
  result
rescue
  rollback_transaction
  raise
end

def with_retry(max_attempts: 3, &operation)
  attempts = 0
  begin
    attempts += 1
    operation.call
  rescue => e
    retry if attempts < max_attempts
    raise
  end
end
  1. ブロックを再利用可能にする
# GOOD: ブロックをインスタンス変数に保存して再利用
class Pipeline
  def initialize(&transformer)
    @transformer = transformer
  end

  def process(items)
    items.map(&@transformer)
  end
end

doubler = Pipeline.new { |x| x * 2 }
puts doubler.process([1, 2, 3]).inspect  #=> [2, 4, 6]
puts doubler.process([4, 5, 6]).inspect  #=> [8, 10, 12]

Ruby 3.4での改善点

  • Prismパーサーによるブロック解析の最適化 - ブロック引数の解析が高速化
  • YJITの最適化 - ブロック呼び出しのインライン化が改善
  • itパラメータ - ブロック引数をitで参照可能(簡潔な記述)
  • エラーメッセージの改善 - ブロック関連のエラーメッセージがより詳細に
# Ruby 3.4の新機能:itパラメータ
numbers = [1, 2, 3, 4, 5]

# 従来の書き方
puts numbers.select { |n| n.even? }.inspect  #=> [2, 4]

# itを使った書き方(Ruby 3.4+)
puts numbers.select { it.even? }.inspect  #=> [2, 4]

まとめ

この記事では、ブロック引数について以下の内容を学びました:

  • 基本概念と重要性 - &パラメータ、block_given?、yieldとcallの使い分け
  • 基本的な使い方と構文 - ブロック引数の定義と呼び出し方法
  • 実践的なユースケース - リソース管理、トランザクション、コールバック、ビルダーパターン、遅延評価
  • 注意点とベストプラクティス - 引数の順序、yieldとcallのパフォーマンス、ブロックの再利用

ブロック引数を適切に使用することで、柔軟で再利用可能なコードを書くことができます。

参考資料

Discussion