💎
【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
注意点とベストプラクティス
注意点
- ブロック引数は最後に配置
# 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
- &の使い分け
# メソッド定義時の&:ブロックを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"]
- 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
ベストプラクティス
- ブロックの有無を明確にする
# 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
- ブロック引数に意味のある名前を付ける
# 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
- ブロックを再利用可能にする
# 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