클로저
클로저 (Closures)
캡처된 블록(captured block)과 proc 리터럴은 지역 변수와 self를 클로저로 감쌉니다. 이게 뭘 의미하는지 예시로 보는 게 이해하기 제일 쉬워요. 또 클로저된 변수의 타입은 컴파일러가 모든 할당의 합집합(union) 타입으로 추론한다는 점도 알아두세요.
본문
캡처된 블록과 proc 리터럴은 지역 변수와 self를 클로저로 감쌉니다. 예시로 보는 게 더 잘 이해돼요:
x = 0
proc = -> { x += 1; x }
proc.call # => 1
proc.call # => 2
x # => 2
아니면 메서드에서 proc을 반환하는 경우도 있어요:
def counter
x = 0
-> { x += 1; x }
end
proc = counter
proc.call # => 1
proc.call # => 2
위 예시에서 x는 지역 변수인데도 proc 리터럴에 의해 캡처됐어요. 이 경우 컴파일러는 x를 힙(heap)에 할당하고 그것을 proc의 컨텍스트 데이터로 써서 동작하게 해요. 평소에는 지역 변수가 스택(stack)에 살다가 메서드가 반환되면 사라지기 때문이에요.
클로저된 변수의 타입 (Type of closured variables)
컴파일러는 보통 지역 변수의 타입을 꽤 적당히 똑똑하게 추론해요. 예를 들어:
def foo(&)
yield
end
x = 1
foo do
x = "hello"
end
x # : Int32 | String
컴파일러는 블록 이후 x가 Int32 또는 String이 될 수 있음을 알아요. (메서드가 항상 yield 하므로 항상 String이 된다는 것까지는 알 수 있는데, 이건 나중에 개선될 수도 있어요.)
블록 이후 x에 다른 값을 할당하면 컴파일러는 타입이 바뀌었다는 걸 알아요:
x = 1
foo do
x = "hello"
end
x # : Int32 | String
x = 'a'
x # : Char
하지만 x가 proc에 의해 클로저되면, 타입은 항상 그 변수에 대한 모든 할당의 혼합 타입이 돼요:
def capture(&block)
block
end
x = 1
capture { x = "hello" }
x = 'a'
x # : Int32 | String | Char
이유는 캡처된 블록이 클래스 변수나 인스턴스 변수에 저장되어 두 명령어 사이에 다른 스레드에서 호출될 수 있기 때문이에요. 컴파일러는 이것을 철저히 분석하지 않아요. 변수가 proc에 캡처되면 그 proc 호출 시점을 알 수 없다고 그냥 가정할 뿐이에요.
이것은 일반 proc 리터럴에서도 마찬가지예요. proc이 호출되거나 저장되지 않았다는 게 명백해도 마찬가지입니다:
x = 1
-> { x = "hello" }
x = 'a'
x # : Int32 | String | Char