> [!abstract] Introduction
> 러스트에서는 마치 어떤 값을 다루는 것처럼 함수를 변수에 저장하거나, 다른 함수에 인자로 전달할 수도 있습니다. 이번 글에서는 값처럼 다루어지는 익명 함수, 클로저*closure*에 대해 알아봅니다.
## 논리를 저장하다
```rust
// unwrap_or_else를 사용하면 Result가 Ok일 때 Ok 안의 값을 반환하고,
// Err일 때 클로저 안의 코드를 호출한다.
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
```
위에 주어진 예시 코드에서 `unwrap_or_else` 메서드 안에 클로저 표현식이 사용되었는데, 이 메서드를 호출한 인스턴스가 의도된 타입이 아닐 때 인자로 주어진 클로저를 실행하게 된다.
사실 클로저 표현식은 [[함수(Rust)|함수]]와 유사한데, `fn`과 소괄호`()`를 쓰지 않는 대신 `||` 사이에 매개변수가 들어가고 중괄호를 통해 함수 본문을 정의하면 된다. 그럼에도 불구하고 클로저가 함수와 동떨어져 보이는 것은 일반적으로 클로저를 작성할 때 매개변수와 반환 값의 타입을 명시하지 않고 때에 따라서는 중괄호`{}`도 생략하기 때문인데, 생략된 코드를 추가하면 다음과 같다.
```rust
let config = Config::build(&args).unwrap_or_else(|err: &str| -> () {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
```
이렇듯 클로저에서 사용되는 매개변수와 반환 값의 타입은 컴파일러가 알아서 추론하기 때문에, 같은 클로저를 여러 번 사용할 때 처음과 다른 타입의 인자를 넣으면 에러가 발생한다. 컴파일러는 클로저가 처음 호출될 때 타입을 확정하고, 이후 호출에서는 그 타입을 강제한다.
```rust
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5); // 에러: String 타입이 확정되었으므로 정수를 넣을 수 없다
```
```
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected struct `String`, found integer
| arguments to this function are incorrect
```
여기까지만 보면 클로저는 이름 없는 함수에 불과해 보인다. 하지만 클로저에는 일반 함수와 근본적으로 다른 특성이 하나 있다.
## 환경 캡처
클로저가 일반 함수와 구분되는 핵심적인 차이점은 클로저가 정의된 환경*environment*에 있는 값을 함께 포착*capture*할 수 있다는 것이다.[^environment-capture] 일반 함수는 자신의 매개변수와 지역 변수만 사용할 수 있지만, 클로저는 자신이 정의된 범위에 있는 변수를 가져와서 사용할 수 있다.
```rust
fn main() {
let list = vec![1, 2, 3];
println!("클로저 정의 전: {:?}", list);
let only_borrows = || println!("클로저 내부에서: {:?}", list);
println!("클로저 호출 전: {:?}", list);
only_borrows();
println!("클로저 호출 후: {:?}", list);
}
```
이 예시에서 `only_borrows` 클로저는 매개변수가 없지만, 바깥 스코프에 있는 `list`를 사용하고 있다. 클로저가 `list`를 읽기만 하므로 불변 참조로 캡처하며, 클로저 정의 이후에도 `list`를 계속 사용할 수 있다. 이러한 환경 캡처는 [[소유권|소유권]] 시스템과 밀접하게 연결되어 있는데, 클로저가 캡처한 값을 어떻게 사용하느냐에 따라 캡처 방식이 달라지기 때문이다.
[^environment-capture]: 환경 캡처란, 클로저가 정의된 렉시컬 스코프*lexical scope*에 존재하는 변수의 값을 클로저 내부로 가져오는 것을 말한다. 이 개념은 함수형 프로그래밍에서 비롯되었으며, JavaScript나 Python 등 다른 언어의 클로저에서도 동일한 원리가 적용된다.
### 캡처의 세 가지 방식
클로저가 환경의 값을 캡처하는 방식은 [[소유권#참조와 대여|참조와 대여]]의 규칙을 그대로 따른다. 러스트 컴파일러는 클로저 본문에서 캡처한 값을 어떻게 사용하는지 분석하여, 가능한 한 가장 제약이 적은 방식을 자동으로 선택한다.
**불변 대여**로 캡처하는 경우, 클로저는 캡처한 값을 읽기만 한다. 앞서 본 `only_borrows` 클로저가 이에 해당한다. 클로저가 살아있는 동안 원본 변수에 대한 불변 참조가 유지되므로, 클로저 정의와 마지막 호출 사이에서도 원본 변수를 읽을 수 있다.
**가변 대여**로 캡처하는 경우, 클로저는 캡처한 값을 변경한다.
```rust
fn main() {
let mut list = vec![1, 2, 3];
println!("클로저 정의 전: {:?}", list);
let mut borrows_mutably = || list.push(7);
// 여기서 println!("{:?}", list)를 사용하면 컴파일 에러가 발생한다.
// 가변 대여가 활성화된 동안 불변 대여를 할 수 없기 때문이다.
borrows_mutably();
println!("클로저 호출 후: {:?}", list);
}
```
`borrows_mutably` 클로저는 `list`에 값을 추가하므로 가변 참조로 캡처한다. 여기서 주목할 점은 [[소유권#가변 참조자|가변 참조자의 규칙]]이 그대로 적용된다는 것이다. 클로저가 가변 참조를 쥐고 있는 동안에는 다른 참조가 존재할 수 없으므로, 클로저 정의와 마지막 호출 사이에서 `list`를 사용하면 컴파일 에러가 발생한다. 클로저의 마지막 호출이 끝나면 가변 대여가 해제되어 다시 `list`를 자유롭게 사용할 수 있다.
**소유권 이전**으로 캡처하는 경우, 클로저가 값의 소유권을 완전히 가져간다. 이 경우 원본 변수는 더 이상 사용할 수 없다.
### `move` 키워드
클로저가 캡처한 값을 읽기만 하더라도 소유권을 강제로 이전시켜야 하는 상황이 있다. 대표적인 경우가 클로저를 다른 스레드*thread*로 보낼 때인데, 이때는 `move` 키워드를 사용한다.
```rust
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("클로저 정의 전: {:?}", list);
thread::spawn(move || println!("스레드에서: {:?}", list))
.join()
.unwrap();
}
```
`move` 키워드를 `||` 앞에 붙이면, 클로저 본문에서 캡처한 값을 어떻게 사용하든 상관없이 소유권이 클로저로 이전된다.[^move-thread] 이 예시에서 `list`는 읽기만 하지만, 새로 생성된 스레드가 메인 스레드보다 먼저 끝날지 나중에 끝날지 알 수 없으므로 참조자의 유효성을 보장할 수 없다. 따라서 `move`로 소유권 자체를 넘겨야 한다. 만약 `move`를 빼면, 클로저가 `list`의 불변 참조를 캡처하는데 그 참조자가 다른 스레드에서 유효하다는 보장이 없으므로 컴파일러가 에러를 발생시킨다.
[^move-thread]: `move`는 모든 캡처 변수의 소유권을 이전시킨다. 만약 `i32`처럼 `Copy` [[Trait|트레이트]]를 구현한 타입이라면 소유권 이전 대신 값의 복사가 이루어진다.
## `Fn` 트레이트
클로저가 환경을 캡처하는 방식은 곧 클로저가 어떤 [[Trait|트레이트]]를 구현하느냐를 결정한다. 러스트는 클로저의 동작 방식을 세 가지 `Fn` 트레이트로 분류하며, 모든 클로저는 이 중 하나 이상을 자동으로 구현한다.[^fn-trait-auto]
[^fn-trait-auto]: 컴파일러가 클로저 본문을 분석하여 캡처 방식을 결정하고, 그에 맞는 트레이트를 자동으로 구현한다. 개발자가 직접 `Fn` 트레이트를 구현할 필요는 없다.
### `FnOnce`
`FnOnce`는 캡처한 값의 소유권을 클로저 밖으로 이동시키는 클로저에 구현된다. 이름에 *Once*가 붙은 이유는 소유권을 이동시키면 그 값을 다시 사용할 수 없으므로, 이 클로저는 한 번만 호출할 수 있기 때문이다.
```rust
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
```
`unwrap_or_else`의 트레이트 바운드가 `FnOnce() -> T`인 이유는 이 메서드가 클로저를 최대 한 번만 호출하기 때문이다. `Option`이 `Some`이면 클로저를 아예 호출하지 않고, `None`일 때만 한 번 호출한다. 모든 클로저는 최소한 `FnOnce`를 구현한다. 한 번은 호출할 수 있어야 클로저로서 의미가 있기 때문이다.
### `FnMut`
`FnMut`는 캡처한 값을 밖으로 이동시키지 않으면서 값을 변경하는 클로저에 구현된다. 여러 번 호출할 수 있다.
```rust
fn main() {
let mut list = vec![1, 2, 3];
println!("정렬 전: {:?}", list);
list.sort_by_key(|r| r.abs());
println!("정렬 후: {:?}", list);
}
```
`sort_by_key`는 각 요소마다 클로저를 호출하므로 여러 번 호출이 가능해야 하며, 트레이트 바운드로 `FnMut`를 요구한다. 만약 클로저 내부에서 캡처한 값을 밖으로 이동시키려 하면 `FnMut`를 구현할 수 없으므로 컴파일 에러가 발생한다.
```rust
fn main() {
let mut list = vec![
String::from("hello"),
String::from("world"),
];
let mut sort_operations = vec![];
let value = String::from("by key called");
// 이 클로저는 value의 소유권을 sort_operations로 이동시키므로
// FnOnce만 구현하며, FnMut가 필요한 sort_by_key에서는 사용할 수 없다.
list.sort_by_key(|r| {
sort_operations.push(value); // 에러!
r.clone()
});
}
```
이 코드에서 `value`는 `push`에 의해 `sort_operations`로 소유권이 이동하기 때문에 클로저는 `FnOnce`만 구현한다. `sort_by_key`는 `FnMut`를 요구하므로 컴파일이 실패한다.
### `Fn`
`Fn`은 캡처한 값을 이동시키지도, 변경하지도 않는 클로저에 구현된다. 환경에서 아무것도 캡처하지 않는 클로저도 여기에 해당한다. `Fn`을 구현하는 클로저는 부작용*side effect* 없이 여러 번 동시에 호출할 수 있어서, 동시성*concurrency* 환경에서 특히 유용하다.
```rust
fn main() {
let list = vec![1, 2, 3];
// Fn을 구현하는 클로저: list를 불변 참조로만 캡처
let print_list = || println!("{:?}", list);
print_list();
print_list(); // 여러 번 호출 가능
}
```
### 트레이트 계층
세 트레이트는 다음과 같은 포함 관계를 형성한다.
```
FnOnce ⊇ FnMut ⊇ Fn
```
`Fn`을 구현하는 클로저는 자동으로 `FnMut`와 `FnOnce`도 구현하고, `FnMut`를 구현하는 클로저는 자동으로 `FnOnce`도 구현한다.[^fn-hierarchy] 따라서 `FnOnce`를 요구하는 자리에 `Fn` 클로저를 넣을 수 있지만, 반대로 `Fn`을 요구하는 자리에 `FnOnce` 클로저를 넣을 수는 없다.
[^fn-hierarchy]: 이 계층 관계는 직관적으로도 이해할 수 있다. 값을 변경하지 않는 클로저(`Fn`)는 당연히 값을 변경하는 자리(`FnMut`)에서도 안전하게 작동하며, 값을 변경하는 클로저는 한 번만 호출하는 자리(`FnOnce`)에서도 문제없이 작동한다.
## 클로저를 매개변수로 받기
클로저를 함수의 매개변수로 받을 때는 [[Generic|제네릭]]과 트레이트 바운드를 사용한다. 아래 함수는 `Fn` 트레이트를 구현하는 어떤 클로저든 받을 수 있다.
```rust
fn apply_to_3<F>(f: F) -> i32
where
F: Fn(i32) -> i32,
{
f(3)
}
fn main() {
let double = |x| x * 2;
println!("{}", apply_to_3(double)); // 6
}
```
여기서 `F: Fn(i32) -> i32`는 "`F`는 `i32`를 받아 `i32`를 반환하는 클로저"라는 뜻이다. 필요에 따라 `Fn` 대신 `FnMut`나 `FnOnce`를 사용하면 된다. 어떤 트레이트 바운드를 선택할지는 클로저를 함수 내부에서 어떻게 사용하느냐에 따라 달라진다.
## 클로저를 반환하기
함수에서 클로저를 반환할 때는 `impl Fn` 문법을 사용한다.[^impl-trait]
```rust
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(3)); // 8
}
```
`make_adder`는 `x`를 캡처한 클로저를 반환한다. 함수가 끝나면 `x`는 스코프를 벗어나므로 `move`를 사용하여 소유권을 클로저로 이전해야 한다. 반환 타입에 `impl Fn(i32) -> i32`를 쓰면 "이 함수는 `Fn(i32) -> i32`를 구현하는 어떤 타입을 반환한다"는 의미가 된다.
[^impl-trait]: `impl Trait` 문법은 [[Generic|제네릭]]의 한 형태로, 호출자가 구체적인 타입을 알 필요 없이 트레이트만 만족하면 되는 상황에서 사용한다. 클로저는 각각 고유한 익명 타입을 가지므로 구체적인 타입 이름을 쓸 수 없어 이 문법이 필수적이다.
## 함수 포인터와의 차이
[[함수(Rust)#함수 포인터|함수 포인터]] `fn`과 클로저의 `Fn` 트레이트는 비슷해 보이지만 근본적으로 다르다. `fn`은 환경을 캡처하지 않는 함수만 가리킬 수 있는 구체적인 타입이고, `Fn`은 클로저와 함수 모두를 포괄하는 트레이트다. 함수 포인터 `fn`은 세 가지 `Fn` 트레이트를 모두 구현하므로, 클로저를 인자로 받는 함수에 일반 함수를 전달하는 것도 가능하다.
```rust
fn add_one(x: i32) -> i32 {
x + 1
}
fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 {
f(val)
}
fn main() {
let closure = |x| x + 1;
println!("{}", apply(closure, 5)); // 클로저 전달: 6
println!("{}", apply(add_one, 5)); // 함수 전달: 6
}
```
일반적으로는 `Fn` 트레이트 바운드를 사용하는 것이 더 유연한데, 클로저와 일반 함수를 모두 받을 수 있기 때문이다.
---
## 참고 자료 & 더보기
+ Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [13. 함수형 언어의 특성: 반복자와 클로저](https://doc.rust-kr.org/ch13-00-functional-features.html).
+ Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [19.4. 고급 함수와 클로저](https://doc.rust-kr.org/ch19-05-advanced-functions-and-closures.html).
+ Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [13.1. 클로저: 자신의 환경을 캡처하는 익명 함수](https://doc.rust-kr.org/ch13-01-closures.html).