고급 함수와 클로저
이 절에서는 함수 포인터와 클로저를 반환하는 기능을 포함, 함수와 클로저와 관련된 고급 기능에 대해 살펴봅니다.
함수 포인터
고급 함수와 클로저에서 컴파일러가 요구하는 규칙과 실패 경로를 정리한 것입니다.
지금까지는 함수에 클로저를 전달하는 방법에 대해 설명했는데, 일반
함수를 함수에 전달할 수도 있습니다! 이 기법은 새로운 클로저를 정의하는
대신 이미 정의한 함수를 전달하고 싶을 때 유용합니다. 함수는 (소문자
f를 쓰는) fn 타입으로 강제되는데, Fn 클로저 트레이트와 혼동하면
안 됩니다. fn 타입을 함수 포인터(function pointer) 라고 합니다.
함수 포인터로 함수를 전달하면 함수를 다른 함수에 대한 인수로 사용할
수 있습니다.
매개변수가 함수 포인터임을 지정하는 문법은 예제 18-27에 나온
것처럼 클로저의 문법과 유사하며, 여기서는 매개변수에 1을 더하는
함수 add_one을 정의했습니다. do_twice 함수는 두 개의 매개변수를
받습니다. i32 매개변수를 받아 i32를 반환하는 함수를 가리키는 함수
포인터와 하나의 i32 값이지요. do_twice 함수는 f 함수를
두 번 호출하여 arg 값을 전달한 다음 두 함수 호출 결과를
합산합니다. main 함수는 add_one 및 5를 인수로 사용하여
do_twice를 호출합니다.
fn 타입을 사용하여 함수 포인터를
인수로 허용하기
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
}
이 코드는 The answer is: 12를 출력합니다. 여기서는 do_twice의 매개변수
f가 i32 타입의 매개변수 하나를 받아 i32를 반환하는 fn임을 지정합니다.
그러면 do_twice의 본문에서 f를 호출할 수 있습니다. main에서는 함수 이름
add_one을 do_twice의 첫 번째 인수로 전달할 수 있습니다.
클로저와 달리 fn은 트레이트가 아닌 타입이므로, Fn 트레이트 중 하나를
트레이트 바운드로 사용한 제네릭 타입 매개변수를 선언하는 대신에 fn을
매개변수 타입으로 직접 지정합니다.
함수 포인터는 세 가지 클로저 트레이트(Fn, FnMut, FnOnce)을 모두
구현하므로, 클로저를 기대하는 함수에 대한 인수로 함수 포인터를 언제나
전달할 수 있습니다. 제네릭 타입과 클로저 트레이트 중 하나를 사용하는
함수를 작성하여 함수나 클로저 중 하나를 받아들일 수 있도록 하는 것이
가장 좋습니다.
즉, 클로저가 아닌 fn만 허용하고 싶은 경우의 한 가지 예로는
클로저가 없는 외부 코드와 상호작용할 때입니다. C 함수는 함수를
인수로 받을 수 있지만, C에는 클로저가 없습니다.
인라인으로 정의된 클로저나 명명된 함수를 사용할 수 있는 예시로,
표준 라이브러리의 Iterator 트레이트가 제공하는 map 메서드의
사용을 살펴봅시다. map 함수를 사용하여 숫자 벡터를 문자열
벡터로 바꾸려면 다음과 같이 클로저를 사용할 수 있습니다.
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
혹은 아래와 같이 클로저 대신 map의 인수로 함수 이름을 지정할
수도 있습니다.
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
to_string이라는 이름의 함수가 여러 개 있을 수 있기 때문에, 앞서
‘고급 트레이트’ 절에서 설명했던
완전 정규화 문법을 사용해야 하는 점을 유의하세요. 여기서는 ToString
트레이트에 정의된 to_string 함수를 사용하고 있는데, 이는 표준
라이브러리에서 Display를 구현하는 모든 타입에 대해 구현되어 있습니다.
5장의 ‘열거형 값’절에서 우리가 정의하는 각 열거형 배리언트의 이름도 이니셜라이저 함수가 된다는 것을 기억해 두세요. 이러한 이니셜라이저 함수는 클로저 트레이트를 구현하는 함수 포인터로써 사용될 수 있는데, 이는 다음과 같이 클로저를 취하는 메서드의 인수로 이니셜라이저 함수를 지정할 수 있음을 뜻합니다.
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
여기서는 Status::Value의 이니셜라이저 함수를 사용하여 map이 호출되는
범위의 각 u32 값을 사용한 Status::Value 인스턴스를 생성합니다. 어떤
사람들은 이 스타일을 선호하고, 어떤 사람들은 클로저를 사용하는 것을 선호합니다.
둘 다 동일한 코드로 컴파일되므로 여러분에게 더 명확한 스타일을 사용하세요.
클로저 반환하기
클로저는 트레이트로 표현되므로, 클로저를 직접 반환할 수 없습니다.
트레이트를 반환해야 하는 대부분의 경우, 대신 트레이트를 구현하는
구체적 타입을 함수의 반환 값으로 사용할 수 있습니다. 그러나,
클로저에는 반환할 수 있는 구체적 타입이 없기 때문에 그렇게 할
수 없습니다. 예를 들어, 클로저가 스코프의 값을 캡처한다면 함수
포인터 fn은 반환 타입으로 사용할 수 없습니다.
다음 코드는 클로저를 직접 반환하려고 시도하지만, 컴파일되지 않습니다.
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
컴파일 에러는 다음과 같습니다.
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error
이 에러는 Sized 트레이트를 다시 언급합니다! 러스트는 클로저를 저장하기 위해
얼마나 많은 공간이 필요한지 알 수 없습니다. 이 문제에 대한 해결책을 이전에
살펴봤었지요. 바로 트레이트 객체가 사용될 수 있습니다.
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
이 코드는 잘 컴파일됩니다. 트레이트 객체에 대한 자세한 내용은 16장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’절을 참고하세요.
다음으로, 매크로를 살펴봅시다!
아래 다이어그램은 함수 포인터와 클로저를 인수와 반환값에서 선택하는 기준을 정리합니다.
함수와 클로저를 넘길 때는 다음 다이어그램처럼 캡처 여부와 저장 방식에 따라 선택하면 좋습니다.
다음 다이어그램은 함수 포인터와 클로저 반환에서 캡처 여부와 저장 방식이 타입 선택을 어떻게 바꾸는지 보여줍니다.
고급 함수와 클로저 학습 판단 기준표
고급 함수와 클로저에서는 소유권, 타입, 오류 처리 경계를 실제 코드에서 어떤 순서로 확인할지 정리합니다.