고급 함수와 클로저

이 섹션에서는 함수 포인터와 클로저 반환을 포함해 함수와 클로저와 관련된 몇 가지 고급 기능을 살펴본다.

함수 포인터

클로저를 함수에 전달하는 방법에 대해 알아봤다면, 일반 함수도 함수에 전달할 수 있다! 이 기법은 새로운 클로저를 정의하지 않고 이미 정의된 함수를 전달하고 싶을 때 유용하다. 함수는 Fn 클로저 트레잇과 혼동하지 않도록 fn (소문자 f) 타입으로 강제 변환된다. 이 fn 타입을 함수 포인터 라고 부른다. 함수 포인터를 사용해 함수를 전달하면, 함수를 다른 함수의 인자로 사용할 수 있다.

함수 포인터를 매개변수로 지정하는 문법은 클로저와 유사하다. 아래 예제 20-28에서 add_one 함수는 매개변수에 1을 더한다. do_twice 함수는 두 개의 매개변수를 받는다: 하나는 i32 타입의 매개변수를 받고 i32를 반환하는 함수 포인터, 다른 하나는 i32 값이다. do_twice 함수는 함수 f를 두 번 호출하며 arg 값을 전달한 후, 두 함수 호출 결과를 더한다. main 함수는 add_one5를 인자로 do_twice를 호출한다.

Filename: 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}");
}
Listing 20-28: 함수 포인터를 인자로 받기 위해 fn 타입 사용

이 코드는 The answer is: 12를 출력한다. do_twice의 매개변수 fi32 타입의 매개변수를 하나 받고 i32를 반환하는 fn 타입으로 지정한다. 그런 다음 do_twice 본문에서 f를 호출할 수 있다. main에서는 add_one 함수 이름을 do_twice의 첫 번째 인자로 전달한다.

클로저와 달리 fn은 트레잇이 아닌 타입이므로, Fn 트레잇 중 하나를 트레잇 바운드로 사용해 제네릭 타입 매개변수를 선언하는 대신 fn을 직접 매개변수 타입으로 지정한다.

함수 포인터는 세 가지 클로저 트레잇(Fn, FnMut, FnOnce)을 모두 구현한다. 즉, 클로저를 기대하는 함수에 항상 함수 포인터를 인자로 전달할 수 있다. 함수나 클로저를 모두 받을 수 있도록 제네릭 타입과 클로저 트레잇 중 하나를 사용해 함수를 작성하는 것이 가장 좋다.

그러나 클로저가 없는 외부 코드와 인터페이스할 때는 fn만 받고 싶을 수 있다. C 함수는 함수를 인자로 받을 수 있지만, C에는 클로저가 없다.

인라인으로 정의된 클로저나 이름이 있는 함수를 사용할 수 있는 예시로, 표준 라이브러리의 Iterator 트레잇이 제공하는 map 메서드를 살펴보자. 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();
}
Listing 20-29: map 메서드와 클로저를 사용해 숫자를 문자열로 변환

또는 클로저 대신 함수 이름을 map의 인자로 사용할 수 있다. 예제 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();
}
Listing 20-30: String::to_string 메서드를 사용해 숫자를 문자열로 변환

여기서는 “고급 트레잇”에서 설명한 완전한 문법을 사용해야 한다. 왜냐하면 to_string이라는 이름의 함수가 여러 개 있기 때문이다.

여기서는 표준 라이브러리가 Display를 구현한 모든 타입에 대해 구현한 ToString 트레잇에 정의된 to_string 함수를 사용한다.

6장의 “열거형 값”에서 정의한 각 열거형 변형의 이름이 초기화 함수가 된다는 것을 기억할 것이다. 이 초기화 함수를 클로저 트레잇을 구현한 함수 포인터로 사용할 수 있다. 즉, 클로저를 받는 메서드에 초기화 함수를 인자로 지정할 수 있다. 예제 20-31에서 이를 확인할 수 있다.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Listing 20-31: map 메서드와 열거형 초기화 함수를 사용해 숫자로부터 Status 인스턴스 생성

여기서 map이 호출된 범위의 각 u32 값을 사용해 Status::Value 인스턴스를 생성한다. 이때 Status::Value의 초기화 함수를 사용한다. 어떤 사람은 이 스타일을 선호하고, 어떤 사람은 클로저를 선호한다. 둘 다 같은 코드로 컴파일되므로, 더 명확한 스타일을 사용하면 된다.

클로저 반환하기

클로저는 트레잇으로 표현되기 때문에 직접 반환할 수 없다. 대부분의 경우 트레잇을 반환하려면 해당 트레잇을 구현한 구체적인 타입을 함수의 반환 값으로 사용할 수 있다. 하지만 클로저는 반환 가능한 구체적인 타입이 없기 때문에 일반적으로 이 방법을 사용할 수 없다. 예를 들어, 클로저가 스코프에서 값을 캡처하면 함수 포인터 fn을 반환 타입으로 사용할 수 없다.

대신, 일반적으로 10장에서 배운 impl Trait 문법을 사용한다. Fn, FnOnce, FnMut를 사용해 어떤 함수 타입이든 반환할 수 있다. 예를 들어, 아래 예제 코드는 정상적으로 동작한다.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: impl Trait 문법을 사용해 함수에서 클로저 반환하기

하지만 13장의 “클로저 타입 추론과 명시적 타입 지정”에서 언급했듯이, 각 클로저는 고유한 타입을 가진다. 동일한 시그니처를 가지지만 구현이 다른 여러 함수를 다뤄야 한다면 트레잇 객체를 사용해야 한다. 아래 예제 코드에서 어떤 일이 발생하는지 살펴보자.

Filename: src/main.rs
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
}
Listing 20-33: impl Fn을 반환하는 함수로 정의된 클로저의 Vec<T> 생성하기

여기서 returns_closurereturns_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` (opaque type at <src/main.rs:9:25>)
              found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
   = 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가 우리를 위해 생성한 세부 사항을 볼 수 없는 타입이다. 따라서 이 두 함수가 동일한 트레잇 Fn(i32) -> i32를 구현하는 클로저를 반환하더라도, Rust가 생성한 불투명 타입은 서로 다르다. (이는 17장의 “여러 Future 다루기”에서 본 것처럼, 동일한 출력 타입을 가지는 다른 async 블록에 대해 Rust가 서로 다른 구체적인 타입을 생성하는 것과 유사하다.) 이 문제에 대한 해결책은 이미 여러 번 살펴봤듯이, 트레잇 객체를 사용하는 것이다. 아래 예제 코드를 참고하자.

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)
}
Listing 20-34: Box<dyn Fn>을 반환하는 함수로 정의된 클로저의 Vec<T> 생성하기

이 코드는 정상적으로 컴파일된다. 트레잇 객체에 대한 더 자세한 내용은 18장의 “다양한 타입의 값을 허용하는 트레잇 객체 사용하기” 섹션을 참고하자.

다음으로, 매크로에 대해 알아보자!