블락과 프록

메서드는 코드 블락을 받아서 yield 에약어로 그 블락을 실행할 수 있습니다.

def twice
  yield
  yield
end

twice do
  puts "안녕!"
end

이 프로그램은 각 yield마다 한 번, 총 두 번 "안녕!"을 출력합니다.

내부에서 yield를 사용하면 컴파일러는 메서드가 블락을 받는다는 것을 알 수 있습니다. 더 명백하게 나타내려면 마지막 인자 이름 앞에 & 기호를 붙여서 더미 인자를 선언합니다.

def twice(&block)
  yield
  yield
end

메서드를 호출할 때 블락을 넘기려면 do ... end 혹은 { ... }를 사용합니다. 둘은 같은 의미입니다.

twice() do
  puts "안녕!"
end

twice do
  puts "안녕!"
end

twice { puts "안녕!" }

do ... end{ ... }의 차이점은 do ... end는 왼쪽부터 호출을 구분하고 { ... }는 오른쪽부터 호출을 구분한다는 것입니다.

foo bar do
  something
end

# 다음과 같은 의미
foo(bar) do
  something
end

foo bar { something }

# 다음과 같은 의미

foo(bar { something })

do ... end를 이용함으로써 평범한 영어 같은 도메인 특정 언어(DSL)를 만들 수 있습니다.

open file "foo.cr" do
  something
end

# 다음과 동일
open(file("foo.cr")) do
end

동작이 다음과 같았다면 보기 좋은 DSL을 만드는 데 한층 애로사항이 꽃피었을 것입니다.

open(file("foo.cr") do
end)

오버로드

오버로딩 항목에서 보았던 것처럼, 블락을 받는 메서드와 블락을 받지 않는 메서드는 서로 다른 오버로드로 취급됩니다.

블락에 인자를 넘기기

yield 표현식은 메서드 호출과 같이 인자를 받을 수 있습니다.

def twice
  yield 1
  yield 2
end

twice do |i|
  puts "#{i} 받았다"
end

이 코드는 "1 받았다"와 "2 받았다"를 출력합니다.

중괄호 표기 또한 쓸 수 있습니다.

twice { |i| puts "#{i} 받았다" }

여러 값을 yield에 넘길 수도 있습니다.

def many
  yield 1, 2, 3
end

many do |x, y, z|
  puts x + y + z
end

# 출력: 6

블락에서 정의한 인자보다 많은 인자를 넘길 수 있습니다.

def many
  yield 1, 2, 3
end

many do |x, y|
  puts x + y
end

# 출력: 3

넘겨진 인자보다 블락의 인자가 더 많다면 오류가 발생합니다.

def twice
  yield
  yield
end

twice do |i| # 오류: 블락 인자 너무 많음
end

각 블락 인자는 해당 위치에 넘겨진 인자의 타입을 갖습니다.

def some
  yield 1, 'a'
  yield true, "안녕"
  yield 2, nil
end

some do |first, second|
  # first는 Int32 | Bool
  # second는 Char | String | Nil
end

마지막 yield 표현식에는 두 번째 인자가 없었기 때문에 블락 인자 second의 타입은 Nil 또한 포함합니다.

인자가 하나인 경우의 짧은 문법

블락이 인자 하나를 받아 그 인자의 메서드를 호출하는 경우 짧은 문법으로 블락을 정의할 수 있습니다.

method do |argument|
  argument.some_method
end

이를 다음과 같이 쓸 수 있습니다.

method &.some_method

다음도 동일합니다.

method(&.some_method)

이는 단순히 문법적인 편의 사항으로 성능에는 영향을 미치지 않습니다.

some_method에 인자를 넘길 수 도 있습니다.

method &.some_method(arg1, arg2)

연산자도 마찬가지입니다.

method &.+(2)
method &.[index]

yield 표현식의 값

yield 표현식도 값을 가집니다. 블락의 마지막 값이 바로 yield의 값입니다.

def twice
  v1 = yield 1
  puts v1

  v2 = yield 2
  puts v2
end

twice do |i|
  i + 1
end

"2"와 "3"이 출력됩니다.

yield 표현식의 값은 값을 변환하거나 여과할 때 유용합니다. Enumerable#mapEnumerable#select이 좋은 예입니다.

ary = [1, 2, 3]
ary.map { |x| x + 1 }         #=> [2, 3, 4]
ary.select { |x| x % 2 == 1 } #=> [1, 3]

변환 메서드를 다음과 같이 정의할 수 있습니다.

def transform(value)
  yield value
end

transform(1) { |x| x + 1 } #=> 2

transform 메서드의 마지막 값은 yield이며, 그 값은 블락의 마지막 값이 되므로 이 표현식의 값은 2가 됩니다.

break

블락에서 break 표현식을 사용하면 메서드의 실행을 종료합니다.

def thrice
  puts "1 이전"
  yield 1
  puts "2 이전"
  yield 2
  puts "3 이전"
  yield 3
  puts "3 이후"
end

thrice do |i|
  if i == 2
    break
  end
end

"1 이전"과 "2 이전"이 출력됩니다. thrice 메서드에 넘겨진 블락에서 break을 실행하기 때문에 puts "3 이전" 표현식은 실행되지 않습니다.

break도 인자를 받을 수 있습니다. 이 값이 메서드의 반환 값이 됩니다.

def twice
  yield 1
  yield 2
end

twice { |i| i + 1 } #=> 3
twice { |i| break "안녕" } #=> "안녕"

twice 메서드의 마지막 값은 yield이며, 그 값은 블락의 값이므로 첫 번째 호출의 값은 3입니다. break가 실행되므로 두 번째 호출의 값은 "안녕"입니다.

조건부로 break를 실행한다면 호출의 반환 타입은 블락의 값과 break에 넘겨진 값들의 타입 공용체입니다.

value = twice do |i|
  if i == 1
    break "안녕"
  end
  i + 1
end
value #:: Int32 | String

break가 인자를 여러 개 받으면 그 인자들은 자동으로 Tuple로 변환됩니다.

values = twice { break 1, 2 }
values #=> {1, 2}

break가 인자를 받지 않는 것은 nil 인자를 받는 것과 동일합니다.

value = twice { break }
value #=> nil

next

블락에서의 next 표현식은 (메서드가 아니라) 블락의 실행을 종료합니다.

def twice
  yield 1
  yield 2
end

twice do |i|
  if i == 1
    puts "1 생략"
    next
  end

  puts "#{i}"
end

# 출력:
# 1 생략
# 2

next 표현식이 인자를 받으면 그 값이 블락을 호출한 yield 표현식의 값이 됩니다.

def twice
  v1 = yield 1
  puts v1

  v2 = yield 2
  puts v2
end

twice do |i|
  if i == 1
    next 10
  end

  i + 1
end

# 출력
# 10
# 3

next가 인자를 여러 개 받으면, 그 인자들은 자동으로 Tuple로 변환됩니다. 인자를 받지 않는 것은 nil 인자를 받는 것과 동일합니다.

with ... yield

yield 표현식에 with 예약어를 이용하여 블락에서 리시버로 어떤 객체를 사용할지를 변경할 수 있습니다.

class Foo
  def one
    1
  end

  def yield_with_self
    with self yield
  end

  def yield_normally
    yield
  end
end

def one
  "하나"
end

Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one }  # => "하나"

블락 인자 쪼개기

블락 인자는 괄호 안에서 받은 인자를 쪼갤 수 있습니다.

array = [{1, "하나"}, {2, "둘"}]
array.each do |(number, word)|
  puts "#{number}: #{word}"
end

이는 다음과 동일한 코드를 작성하는 문법적 편의 사항입니다.

array = [{1, "하나"}, {2, "둘"}]
array.each do |arg|
  number = arg[0]
  word = arg[1]
  puts "#{number}: #{word}"
end

이는 정수를 받는 [] 메서드가 있는 타입이라면 블락 인자에서 쪼갤 수 있다는 뜻입니다.

성능

yield를 사용할 때 블락은 항상 인라인됩니다. 클로저도, 호출도, 함수 포인터도 없습니다.

def twice
  yield 1
  yield 2
end

twice do |i|
  puts "#{i} 받았다"
end

다음을 쓰는 것과 완전히 동일합니다.

i = 1
puts "#{i} 받았다"
i = 2
puts "#{i} 받았다"

한 예로, 표준 라이브러리에 선언된 정수 타입의 메서드 times가 있습니다.

3.times do |i|
  puts i
end

꽤 멋있어 보이는데 반복이 과연 C만큼 빠를까요? 네, 그렇습니다.

Int#times의 정의는 다음과 같습니다.

struct Int
  def times
    i = 0
    while i < self
      yield i
      i += 1
    end
  end
end

포획되지 않은 블락은 항상 인라인되기 때문에 위의 메서드 호출은 다음과 완전히 동일합니다.

i = 0
while i < 3
  puts i
  i += 1
end

재사용성이나 가독성을 위해 블락을 사용하는 것을 겁낼 필요가 없습니다. 실행되는 코드의 성능에는 아무 영향도 없습니다.

results matching ""

    No results matching ""