블록 전달

블록 전달 (Block forwarding)

잡아둔(captured) 블록을 그대로 전달하고 싶을 때가 있어요. 그럴 때는 블록 인자(block argument)를 쓰고, 식 앞에 &를 붙이면 돼요:

def capture(&block)
  block
end

def invoke(&block)
  block.call
end

proc = capture { puts "Hello" }
invoke(&proc) # prints "Hello"

위 예시에서 invoke는 블록을 받아요. 그런데 invoke는 일반 인자를 받지 않고 블록 인자만 받기 때문에, proc을 그대로 넘길 수는 없어요. 그래서 &를 써서 proc을 진짜 블록 인자로 넘기고 싶다는 걸 명확히 해주는 거예요. 그렇게 하지 않으면:

invoke(proc) # Error: wrong number of arguments for 'invoke' (1 for 0)

yield하는 메서드에 proc을 넘겨줄 수도 있어요:

def capture(&block)
  block
end

def twice(&)
  yield
  yield
end

proc = capture { puts "Hello" }
twice &proc

위 코드는 단순히 이렇게 다시 쓰인 것과 같아요:

proc = capture { puts "Hello" }
twice do
  proc.call
end

아니면 &-> 문법을 조합할 수도 있어요:

twice &-> { puts "Hello" }

또는:

def say_hello
  puts "Hello"
end

twice &->say_hello

출처: Crystal 공식 문서

본문

잡아두지 않은 블록 전달하기 (Forwarding non-captured blocks)

잡아두지 않은(non-captured) 블록을 전달하려면 yield를 써야 해요:

def foo(&)
  yield 1
end

def wrap_foo(&)
  puts "Before foo"
  foo do |x|
    yield x
  end
  puts "After foo"
end

wrap_foo do |i|
  puts i
end

# Output:
# Before foo
# 1
# After foo

블록 전달에 &block 문법을 쓸 수도 있어요. 다만 그러려면 최소한 입력 타입을 지정해야 하고, 생성되는 코드가 클로저를 수반해서 더 느려져요:

def foo(&)
  yield 1
end

def wrap_foo(&block : Int32 -> _)
  puts "Before foo"
  foo(&block)
  puts "After foo"
end

wrap_foo do |i|
  puts i
end

# Output:
# Before foo
# 1
# After foo

yield로 충분하다면 이렇게 블록을 전달하는 건 피하는 게 좋아요. 또, 잡아둔 블록 안에서는 breaknext를 쓸 수 없다는 문제도 있어요. 그래서 &block 전달을 쓰면 다음 코드는 작동하지 않아요:

foo_forward do |i|
  break # error
end

요약하면, yield가 연관된 상황에서는 &block 전달을 피하는 게 좋아요.

더 알아보기

  • 잡아둔 블록은 &를 붙여 전달하고, 잡아두지 않은 블록은 yield로 전달해요.
  • &block 전달은 입력 타입을 지정해야 하고 클로저로 인해 더 느려지므로, yield만으로 충분하면 되도록 피하세요.
  • 잡아둔 블록 안에서는 breaknext를 쓸 수 없다는 점을 기억해 두세요.