고급 함수와 클로저
고급 함수와 클로저 (Advanced Functions and Closures)
이 절에서는 함수와 클로저에 관련된 고급 기능 몇 가지를 탐구해요. 함수 포인터와 클로저 반환을 포함해서요.
출처: The Rust Book
함수 포인터 (Function Pointers)
클로저를 함수에 넘기는 방법은 이미 다뤘어요. 그런데 일반 함수도 함수에 넘길 수 있다는 걸 아나요? 새 클로저를 정의하는 대신 이미 정의한 함수를 넘기고 싶을 때 이 기법이 유용해요. 함수는 fn 타입(소문자 f)으로 강제(coerce)되는데, Fn 클로저 트레이트와 혼동하지 말아야 해요. fn 타입을 _함수 포인터(function pointer)_라고 불러요. 함수 포인터로 함수를 넘기면 함수를 다른 함수의 인자로 쓸 수 있어요.
파라미터가 함수 포인터임을 지정하는 문법은 클로저의 문법과 비슷해요. Listing 20-28이 보여주죠. 여기서 파라미터에 1을 더하는 add_one 함수를 정의했어요. do_twice 함수는 두 파라미터를 받아요. 하나는 i32 파라미터를 받고 i32를 반환하는 어떤 함수에 대한 함수 포인터이고, 다른 하나는 i32 값이에요. do_twice 함수는 함수 f를 두 번 호출하면서 arg 값을 넘기고, 두 함수 호출 결과를 더해요. main 함수는 do_twice를 add_one과 5 인자로 호출해요.
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 메서드로 숫자 벡터를 문자열 벡터로 바꾸려면 Listing 20-29처럼 클로저를 쓸 수 있어요.
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의 인자로 이름을 붙일 수도 있어요. Listing 20-30이 그 모습을 보여줘요.
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이라는 이름의 함수가 여러 개 있으므로, "Advanced Traits" 절에서 다룬 완전 한정 문법을 써야 한다는 점을 주목하세요.
여기서는 ToString 트레이트에 정의된 to_string 함수를 쓰는데, 표준 라이브러리는 Display를 구현하는 어떤 타입에 대해서든 이 함수를 구현해 둬요.
6장의 "Enum Values" 절에서, 우리가 정의하는 각 enum 변형(variant)의 이름이 초기화 함수(initializer function)가 된다는 걸 떠올려 보세요. 이 초기화 함수들을 클로저 트레이트를 구현하는 함수 포인터로 쓸 수 있어요. 즉 클로저를 받는 메서드의 인자로 초기화 함수를 지정할 수 있다는 뜻이죠. Listing 20-31이 그걸 보여줘요.
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 인스턴스를 만들어요. 어떤 사람들은 이 스타일을 선호하고 어떤 사람들은 클로저를 선호해요. 둘 다 같은 코드로 컴파일되니, 더 명확해 보이는 스타일을 쓰세요.
클로저 반환하기 (Returning Closures)
클로저는 트레이트로 표현되므로, 클로저를 직접 반환할 수 없어요. 트레이트를 반환하고 싶어 하는 대부분의 경우, 대신 트레이트를 구현하는 구체 타입을 함수의 반환 값으로 쓸 수 있어요. 하지만 보통 클로저에는 그렇게 할 수 없어요. 클로저는 반환 가능한 구체 타입이 없거든요. 예를 들어 클로저가 스코프로부터 어떤 값을 캡처한다면 함수 포인터 fn을 반환 타입으로 쓸 수 없어요.
대신 10장에서 배운 impl Trait 문법을 보통 쓰게 돼요. Fn, FnOnce, FnMut를 사용해 어떤 함수 타입이든 반환할 수 있어요. 예를 들어 Listing 20-32의 코드는 문제없이 컴파일돼요.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
하지만 13장의 "Inferring and Annotating Closure Types" 절에서 언급했듯이, 각 클로저는 그 자체로 별개의 타입이에요. 같은 시그니처지만 다른 구현을 가진 여러 함수를 다뤄야 한다면, 트레이트 객체를 사용해야 해요. Listing 20-33에 나온 코드를 작성하면 어떻게 되는지 생각해 보세요.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
여기 두 함수 returns_closure와 returns_initialized_closure가 있는데 둘 다 impl Fn(i32) -> i32를 반환해요. 같은 타입을 구현하더라도 반환하는 클로저가 다르다는 점을 주목하세요. 컴파일하려 하면 Rust가 이게 동작하지 않을 것이라고 알려줘요.
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
오류 메시지는 우리가 impl Trait를 반환할 때마다 Rust가 고유한 _불투명 타입(opaque type)_을 만든다고 알려줘요. 불투명 타입은 Rust가 우리를 위해 만들어 주는 것의 세부사항을 들여다볼 수 없고, Rust가 만들 타입을 짐작해 직접 쓸 수도 없는 타입이죠. 그래서 이 함수들이 같은 트레이트 Fn(i32) -> i32를 구현하는 클로저를 반환하더라도, Rust가 각각에 대해 만드는 불투명 타입은 서로 달라요. (이는 서로 다른 async 블록이 같은 출력 타입을 갖더라도 Rust가 그들에 대해 다른 구체 타입을 만들어내는 것과 비슷해요. 17장의 "The Pin Type and the Unpin Trait"에서 봤죠.) 이 문제에 대한 해결책을 이미 몇 번 봤어요. Listing 20-34처럼 트레이트 객체를 쓸 수 있어요.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
이 코드는 문제없이 컴파일돼요. 트레이트 객체에 대해 더 알고 싶다면 18장의 "Using Trait Objects To Abstract over Shared Behavior" 절을 참고하세요.
다음으로는 매크로를 살펴볼게요!