고급 함수와 클로저
이 절에서는 함수와 클로저와 관련된 몇 가지 고급 기능을 살펴봅니다. 함수 포인터와, 클로저를 반환하는 방법이 여기에 포함됩니다.
함수 포인터
우리는 클로저를 함수에 인수로 넘기는 방법을 이미 보았습니다. 그런데 클로저뿐 아니라
일반 함수도 함수에 인수로 넘길 수 있습니다! 이는 새로운 클로저를 즉석에서 정의하는
대신 이미 정의해 둔 함수를 재사용하고 싶을 때 유용합니다. 함수는 Fn 클로저
트레이트가 아니라 소문자 f 를 쓰는 fn 타입으로 강제(coerce)됩니다. 이 fn
타입을 함수 포인터(function pointer) 라고 부릅니다. 함수 포인터를 사용하면
함수도 다른 함수의 인수로 넘길 수 있습니다.
어떤 매개변수가 함수 포인터임을 나타내는 문법은 클로저 문법과 비슷합니다. 목록 20-28은
매개변수에 1을 더하는 add_one 함수와, “하나의 i32 를 받아 i32 를 반환하는
함수 포인터”와 하나의 i32 값을 매개변수로 받는 do_twice 함수를 보여 줍니다.
do_twice 는 함수 f 를 arg 값과 함께 두 번 호출하고, 그 두 결과를 다시
더해 반환합니다. main 에서는 add_one 과 5 를 인수로 do_twice 를 호출합니다.
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}");
}
fn 타입 사용하기이 코드는 The answer is: 12 를 출력합니다. 여기서 우리는 do_twice 의 f
매개변수가 “i32 를 받아 i32 를 반환하는 함수 포인터”라고 명시했습니다.
그러면 do_twice 본문 안에서 f 를 일반 함수처럼 호출할 수 있습니다. main
에서는 함수 이름 add_one 자체를 첫 번째 인수로 넘깁니다.
클로저와 달리 fn 은 트레이트가 아니라 타입 이기 때문에, 매개변수 타입으로 그냥
직접 fn 을 적어 줍니다. 제네릭 타입 매개변수에 Fn 계열 트레이트를 바운드로 거는
방식과는 다릅니다.
함수 포인터는 세 개의 클로저 트레이트(Fn, FnMut, FnOnce)를 모두 구현합니다.
그래서 어떤 함수가 클로저를 기대할 때는 일반 함수 포인터를 언제나 대신 넘길 수 있습니다.
그렇기 때문에 보통은 함수 시그니처를 “제네릭 + 클로저 트레이트 바운드” 방식으로
작성하는 것이 좋습니다. 그러면 일반 함수도, 클로저도 둘 다 받아들일 수 있기 때문입니다.
다만 함수만 받고 클로저는 받지 않기를 원하는 대표적 상황이 하나 있습니다. 바로 외부 코드와 연동할 때입니다. 예를 들어 C 함수는 함수를 인수로 받을 수 있지만, C 자체에는 클로저라는 개념이 없습니다.
클로저를 직접 적든, 이미 이름 붙은 함수를 넘기든 둘 다 가능한 예로, 표준 라이브러리의
Iterator 트레이트가 제공하는 map 메서드를 생각해 봅시다. 숫자 벡터를 문자열
벡터로 바꾸기 위해, 목록 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 메서드에 클로저를 넘겨 숫자를 문자열로 바꾸기또는 목록 20-30처럼 클로저 대신 함수 이름을 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();
}
map 메서드와 함께 String::to_string 함수 사용해 숫자를 문자열로 바꾸기여기서는 같은 이름의 함수가 여러 곳에 존재하기 때문에, [“고급 트레이트”] advanced-traits 절에서 봤던 완전 수식 문법을 써야 한다는 점에 주목하세요.
여기서 우리가 사용하는 것은 ToString 트레이트에 정의된 to_string 함수입니다.
표준 라이브러리는 Display 를 구현하는 모든 타입에 대해 ToString 을 자동으로
구현해 둡니다.
또한 6장의 “enum 값” 절에서 보았듯, 우리가 정의한 각 enum variant 이름은 동시에 그 variant를 만드는 생성자 함수 역할도 합니다. 이런 생성자 함수도 클로저 트레이트를 구현하는 함수 포인터처럼 쓸 수 있으므로, 클로저를 받는 메서드에 인수로 넘길 수 있습니다. 목록 20-31을 보세요.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map 메서드와 함께 사용해 숫자로부터 Status 인스턴스 만들기여기서는 map 이 순회하는 범위 안 각 u32 값에 대해, Status::Value 생성자
함수를 사용해 Status::Value 인스턴스를 만듭니다. 어떤 사람은 이 스타일을 더
좋아하고, 또 어떤 사람은 클로저가 더 읽기 쉽다고 느낍니다. 결국 둘은 같은 코드로
컴파일되므로, 여러분에게 더 명확한 스타일을 선택하면 됩니다.
클로저 반환하기
클로저는 트레이트로 표현되기 때문에, 클로저를 함수 반환 타입으로 직접 적을 수는
없습니다. 보통 “어떤 트레이트를 구현하는 것을 반환하고 싶다”는 상황에서는, 그
트레이트를 구현하는 구체 타입 을 반환 타입으로 직접 적으면 됩니다. 하지만 클로저는
대개 “직접 적을 수 있는 구체 타입 이름”이 없기 때문에, 예를 들어 클로저가 환경
값을 캡처하는 경우 함수 포인터 fn 도 반환 타입으로 쓸 수 없습니다.
대신 보통은 10장에서 배운 impl Trait 문법을 사용합니다. Fn, FnOnce,
FnMut 중 적절한 것을 사용해 어떤 함수형 타입이든 반환할 수 있습니다. 예를 들어
목록 20-32의 코드는 문제없이 컴파일됩니다.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl Trait 문법으로 함수에서 클로저 반환하기하지만 13장의 “클로저 타입 추론과 주석” 절에서 언급했듯, 각 클로저는 모두 자기만의 별도 타입입니다. 따라서 시그니처는 같지만 구현이 다른 여러 함수를 하나로 모아 다루고 싶다면, 결국 트레이트 객체를 사용해야 합니다. 목록 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
}
impl Fn 을 반환하는 함수들이 만든 클로저를 하나의 Vec<T> 에 넣으려 시도하기여기에는 returns_closure 와 returns_initialized_closure 라는 두 함수가 있고,
둘 다 impl Fn(i32) -> i32 를 반환합니다. 하지만 이 함수들이 실제로 반환하는
클로저는 서로 다릅니다. 비록 같은 트레이트를 구현하더라도, 이 코드를 컴파일하려
하면 러스트는 다음과 같이 알려 줍니다.
$ 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 를 반환할 때마다 러스트가 그 함수에 대해 하나의
고유한 불투명 타입(opaque type) 을 만든다고 말합니다. 즉, 러스트가 우리 대신
만든 그 타입 내부가 정확히 무엇인지 우리는 들여다볼 수도 없고, 이름을 추측해 직접
쓸 수도 없습니다. 따라서 이 함수들이 모두 같은 Fn(i32) -> i32 트레이트를
구현하는 클로저를 반환한다 하더라도, 러스트가 각 함수마다 생성한 불투명 타입은 서로
다릅니다. (이는 17장에서 본 것처럼, 출력 타입이 같더라도 각 async 블록이 서로 다른
구체 타입을 만들어 내는 것과 비슷합니다.) 이 문제를 해결하는 방법은 이미 여러 번
봤습니다. 바로 트레이트 객체입니다. 목록 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)
}
Box<dyn Fn> 을 반환하는 함수가 만든 클로저를 하나의 Vec<T> 로 다루기이 코드는 문제없이 컴파일됩니다. 트레이트 객체에 대한 더 자세한 내용은 18장의 “공통 동작을 추상화하기 위해 트레이트 객체 사용하기” 절을 참고하세요.
이제 다음으로 매크로를 살펴보겠습니다!