Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

고급 함수와 클로저

이 절에서는 함수와 클로저와 관련된 몇 가지 고급 기능을 살펴봅니다. 함수 포인터와, 클로저를 반환하는 방법이 여기에 포함됩니다.

함수 포인터

우리는 클로저를 함수에 인수로 넘기는 방법을 이미 보았습니다. 그런데 클로저뿐 아니라 일반 함수도 함수에 인수로 넘길 수 있습니다! 이는 새로운 클로저를 즉석에서 정의하는 대신 이미 정의해 둔 함수를 재사용하고 싶을 때 유용합니다. 함수는 Fn 클로저 트레이트가 아니라 소문자 f 를 쓰는 fn 타입으로 강제(coerce)됩니다. 이 fn 타입을 함수 포인터(function pointer) 라고 부릅니다. 함수 포인터를 사용하면 함수도 다른 함수의 인수로 넘길 수 있습니다.

어떤 매개변수가 함수 포인터임을 나타내는 문법은 클로저 문법과 비슷합니다. 목록 20-28은 매개변수에 1을 더하는 add_one 함수와, “하나의 i32 를 받아 i32 를 반환하는 함수 포인터”와 하나의 i32 값을 매개변수로 받는 do_twice 함수를 보여 줍니다. do_twice 는 함수 farg 값과 함께 두 번 호출하고, 그 두 결과를 다시 더해 반환합니다. 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_twicef 매개변수가 “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();
}
Listing 20-29: 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();
}
Listing 20-30: 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();
}
Listing 20-31: enum 생성자를 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
}
}
Listing 20-32: impl Trait 문법으로 함수에서 클로저 반환하기

하지만 13장의 “클로저 타입 추론과 주석” 절에서 언급했듯, 각 클로저는 모두 자기만의 별도 타입입니다. 따라서 시그니처는 같지만 구현이 다른 여러 함수를 하나로 모아 다루고 싶다면, 결국 트레이트 객체를 사용해야 합니다. 목록 20-33이 그런 상황을 보여 줍니다.

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 를 반환합니다. 하지만 이 함수들이 실제로 반환하는 클로저는 서로 다릅니다. 비록 같은 트레이트를 구현하더라도, 이 코드를 컴파일하려 하면 러스트는 다음과 같이 알려 줍니다.

$ 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)
}
Listing 20-34: Box<dyn Fn> 을 반환하는 함수가 만든 클로저를 하나의 Vec<T> 로 다루기

이 코드는 문제없이 컴파일됩니다. 트레이트 객체에 대한 더 자세한 내용은 18장의 “공통 동작을 추상화하기 위해 트레이트 객체 사용하기” 절을 참고하세요.

이제 다음으로 매크로를 살펴보겠습니다!