고급 함수와 클로저
이 절에서는 함수 포인터와 클로저를 반환하는 기능을 포함, 함수와 클로저와 관련된 고급 기능에 대해 살펴봅니다.
함수 포인터
지금까지는 함수에 클로저를 전달하는 방법에 대해 설명했는데, 일반
함수를 함수에 전달할 수도 있습니다! 이 기법은 새로운 클로저를 정의하는
대신 이미 정의한 함수를 전달하고 싶을 때 유용합니다. 함수는 (소문자
f를 쓰는) fn
타입으로 강제되는데, Fn
클로저 트레이트와 혼동하면
안 됩니다. fn
타입을 함수 포인터 (function pointer) 라고 합니다.
함수 포인터로 함수를 전달하면 함수를 다른 함수에 대한 인수로 사용할
수 있습니다.
매개변수가 함수 포인터임을 지정하는 문법은 예제 19-27에 나온
것처럼 클로저의 문법과 유사하며, 여기서는 매개변수에 1을 더하는
함수 add_one
을 정의했습니다. do_twice
함수는 두 개의 매개변수를
받습니다: i32
매개변수를 받아 i32
를 반환하는 함수를 가리키는 함수
포인터와 하나의 i32
값이지요. do_twice
함수는 f
함수를
두 번 호출하여 arg
값을 전달한 다음 두 함수 호출 결과를
합산합니다. main
함수는 add_one
및 5
를 인수로 사용하여
do_twice
를 호출합니다.
파일명: src/main.rs
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
함수를 사용하여 숫자 벡터를 문자열
벡터로 바꾸려면 다음과 같이 클로저를 사용할 수 있습니다:
fn main() { 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
의 인수로 함수 이름을 지정할
수도 있습니다:
fn main() { 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
를 구현하는 모든 타입에 대해 구현되어 있습니다.
6장의 ‘열거형 값’절에서 우리가 정의하는 각 열거형 배리언트의 이름도 이니셜라이저 함수가 된다는 것을 기억해 두세요. 이러한 이니셜라이저 함수는 클로저 트레이트를 구현하는 함수 포인터로써 사용될 수 있는데, 이는 다음과 같이 클로저를 취하는 메서드의 인수로 이니셜라이저 함수를 지정할 수 있음을 뜻합니다:
fn main() { 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)
}
이 코드는 잘 컴파일됩니다. 트레이트 객체에 대한 자세한 내용은 17장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’절을 참고하세요.
다음으로, 매크로를 살펴봅시다!