블록은 메서드에 전달할 수 있는 코드 조각이에요. 다른 프로그래밍 언어에서는 이를 익명 함수나 람다라고 불러요. 메서드는 블록을 Proc으로 변환할 수 있는데, Proc은 코드 블록을 나타내요.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
블록과 Proc은 Crystal에서 반복이나 콜백 같은 곳에 아주 흔히 쓰여요. DSL(도메인 특화 언어)을 만드는 데에도 쓰이는데, DSL의 예로는 웹 프레임워크인 Kemal이나 테스트 프레임워크인 Spec이 있어요. Spec은 바로 Crystal 풀이를 테스트할 때 사용하는 것이에요.
블록을 받을 수 있는 메서드를 만들려면 마지막 인자 앞에 앰퍼샌드(&)를 붙여야 해요.
그러면 블록이 Proc으로 변환되어 그 인자에 할당돼요.
그다음 yield를 사용하면 메서드에 전달된 블록이 실행돼요.
def my_method(&block)
1 + yield
end
블록을 전달하는 방법에는 두 가지가 있는데, do ... end 키워드를 쓰거나 중괄호({})를 쓰는 거예요.
둘의 가장 큰 차이는 우선순위예요. do ... end는 여러 줄 블록을 지원하지만 중괄호({})는 그렇지 않아요.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield는 메서드에 전달된 블록을 실행하는 키워드예요.
즉, yield를 호출하지 않으면 블록은 절대 실행되지 않아요.
yield에는 블록으로 전달될 인자를 줄 수도 있어요.
블록에 전달할 수 있는 인자 수에는 제한이 없어요.
블록 안에서 인자를 사용하려면 |x, y, ...| 문법을 써요. 여기서 x, y, ...는 인자의 이름이에요.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
위 예시에서 블록은 인자 2를 받아 3을 곱한 뒤 6을 반환해요.
그러면 메서드는 그 결과에 1을 더해 7을 반환해요.
yield에 전달하는 인자 수가 블록이 기대하는 인자 수보다 많으면 오류가 발생해요.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
블록은 명시적으로 쓸 수도 있고 암시적으로 쓸 수도 있어요. 암시적 블록이 가장 흔하고, 위 예시에서 본 것이 바로 그런 블록이에요. 블록에 서로 다른 타입의 인자를 줄 수도 있는데, 그러면 컴파일러가 타입을 추론해서 유니언 타입을 만들어요.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
명시적 블록을 작성할 때는 인자의 타입과 반환 타입을 지정할 수 있어요.
이때 Arg1Type, Arg2Type, ... -> ResultType 문법을 사용해요.
여기서 인자는 쉼표(,)로 구분하고, 결과 타입은 ->로 구분해요.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
다른 명시적 타입과 마찬가지로, 컴파일러는 타입이 기대한 대로인지 강제해요. 블록이 메서드에 지정한 타입과 맞지 않으면 오류가 발생해요.
인자를 하나만 받고 그 인자에 대해 메서드를 하나만 호출하는 블록에는 축약 문법이 있어요.
|x|를 쓰는 대신 인자 이름 앞에 &를 쓸 수 있어요.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
Proc은 블록과 아주 밀접하게 연결되어 있어요.
가장 큰 차이는 Proc은 타입이고 블록은 그렇지 않다는 거예요.
Proc은 함수 포인터를 나타내며 변수처럼 주고받을 수 있어요.
Proc을 정의하려면 proc 리터럴 ->(args) { ... } 문법을 사용해요.
인자는 쉼표(,)로 구분하고, 블록은 { ... }로 구분해요.
Proc을 호출하려면 call 메서드를 사용해서 인자를 전달해요.
매개변수의 타입은 필수예요.
반환 타입은 proc의 본문에서 추론되지만, 명시적으로 지정할 수도 있어요.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
블록 캡처는 블록으로부터 Proc을 만드는 방법이에요.
메서드의 마지막 인자 앞에 & 연산자를 사용하면 돼요.
그러면 블록이 Proc으로 변환되어 그 인자에 할당돼요.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Proc은 블록을 기대하는 메서드에 전달할 수 있어요. 어떤 조건에 따라 서로 다른 블록을 전달하고 싶을 때 유용해요.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
변수를 선언할 때 : Type 문법으로 변수의 타입을 선언할 수 있어요.
Proc의 경우에는 Proc((ArgType, ...), ResultType) 문법을 사용해요.
마지막 타입은 항상 반환 타입이고, 나머지는 인자 타입이에요.
즉, 타입이 하나뿐이라면 그것이 반환 타입이고 proc은 인자를 받지 않아요.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
메서드에서 proc을 선언할 수도 있어요.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)