블록은 메서드에 전달할 수 있는 코드 조각이에요. 다른 프로그래밍 언어에서는 익명 함수 또는 람다라고 불러요. 메서드는 블록을 코드 블록을 나타내는 Proc으로 변환할 수 있어요.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
블록과 Proc은 Crystal에서 반복이나 콜백 같은 곳에 아주 흔히 쓰여요. 또한 DSL(도메인 특화 언어)을 만들 때도 쓰이는데, DSL의 예로는 웹 프레임워크인 Kemal이나 테스트 프레임워크인 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
다른 명시적 타입과 마찬가지로, 컴파일러는 타입이 기대한 대로인지 강제해요. 블록이 메서드에 지정한 타입과 맞지 않으면 오류가 발생해요.
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 }
기사, 블로그 글, 그 밖의 여러 글 콘텐츠를 제공하는 웹사이트를 위한 콘텐츠 관리 시스템을 개발하고 있어요. 여러분은 우선순위 큐에 있는 작업을 처리할 작업 핸들러를 구현하는 일을 맡았어요. 작업 핸들러는 작업을 받아서, 서버의 부하와 작업의 우선순위를 바탕으로 다음에 어떤 작업을 실행할지 결정해요. 서버에 부하가 많이 걸려 있으면, 작업이 실행되려면 우선순위 숫자가 더 높아야 해요. 서버는 부하에 따라 부하 수준을 동적으로 바꿔요.
TaskHandler 초기화TaskHandler는 큐와 기본 load_level로 초기화해야 해요.
TaskHandler 클래스의 초기화 메서드를 구현해요. 이 메서드는 작업 이름들의 String Array를 인자로 받아 @tasks라는 인스턴스 변수에 저장해요.
초기화 메서드는 @task_condition_logic 인스턴스 변수도 만들어야 해요. 이 변수에는 Int32를 받아서 인자가 0 이상이면 true를, 0보다 작으면 false를 반환하는 Proc이 저장돼요.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
서버는 서버의 부하에 따라 작업 조건 로직을 업데이트할 수 있어야 해요.
블록을 인자로 받는 TaskHandler#update_task_condition_logic 메서드를 구현해요. 이 블록은 @task_condition_logic 인스턴스 변수에 저장돼야 해요.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
# => #<TaskHandler:0x7f068118cf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562df99d0370>>
서버는 큐에 있는 다음 작업을 실행할 수 있어야 해요.
@tasks 배열의 첫 번째 작업이 다음에 실행할 작업이에요.
주어진 작업의 우선순위 레벨을 @task_condition_logic proc에 넘겼을 때 true가 반환되면 그 작업을 실행해야 해요.
작업이 실행됐는지에 따라 메시지를 반환해요.
작업이 실행되면 "Completed <task>"를 반환해야 해요.
작업이 실행되지 않으면 "Could not complete <task>"를 반환해야 해요.
작업이 실행되면 @tasks 배열에서 제거돼야 해요.
다음 작업의 우선순위 레벨을 Int32로 받는 TaskHandler#execute_next_task 메서드를 구현해요. 이 메서드는 작업이 실행됐는지에 따라 String을 반환해요.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
task_handler.execute_next_task(2)
# => "Completed task1"
task_handler.execute_next_task(1)
# => "Could not complete task2"
Exercism에 가입하고 Crystal 트랙을 개념 26개연습 문제 142개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.