hyunsu blog

홈강좌Rust 입문·중급 47편 중 36편째

Rust 클로저 — 이름 없는 작은 함수가 주변 변수를 잡는다

목차 · 5
  1. 01|x| 식 — 짧게 쓰는 함수
  2. 02타입은 처음 부른 값으로 정해진다
  3. 03바깥 변수를 고치는 클로저
  4. 04실습 — 동작을 넘겨받는 메서드
  5. 05자주 묻는 질문

Rust 클로저는 |매개변수| 식 으로 쓰는 이름 없는 함수다. 변수에 담아 부르고, 만든 자리의 변수를 그대로 쓸 수 있다(캡처). 타입은 처음 부른 값으로 정해져서 다른 타입으로 다시 부르면 E0308 이 난다. sort_by_key·retain 처럼 동작을 넘겨받는 메서드에 쓴다.

01 |x| 식 — 짧게 쓰는 함수

rust
fn main() {
    let add = |a: i32, b: i32| a + b;
    let double = |x: i32| x * 2;
    println!("{}", add(3, 4));
    println!("{}", double(21));

    let tax_rate = 0.1;
    let with_tax = |price: f64| price * (1.0 + tax_rate);
    println!("{}", with_tax(10000.0));
}
text
   Compiling closures v0.1.0 (~/projects/closures)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/closures`
7
42
11000
  • |a: i32, b: i32| a + b 는 "a·b 를 받아 a + b 를 돌려주는 함수" 다. 세로 막대 사이가 매개변수, 그 뒤가 몸체다. 몸체가 길면 중괄호로 감싼다.
  • 변수 add 에 담아 add(3, 4) 처럼 함수와 똑같이 부른다.
  • 세 번째 with_tax 는 몸체에서 바깥 변수 tax_rate 를 쓴다. fn 으로 만든 함수는 바깥 변수를 못 쓰지만, 클로저는 만든 자리의 변수를 잡아서(capture) 쓴다.

02 타입은 처음 부른 값으로 정해진다

매개변수 타입을 안 적으면 컴파일러가 추론한다. 그런데 한 번 정해지면 바뀌지 않는다.

rust
fn main() {
    let same = |x| x;
    let a = same(5);
    let b = same("five");
    println!("{a} {b}");
}
text
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let b = same("five");
  |             ---- ^^^^^^ expected integer, found `&str`
  |             |
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `{integer}`
 --> src/main.rs:3:18
  |
3 |     let a = same(5);
  |             ---- ^ expected because this argument is of type `{integer}`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:17
  |
2 |     let same = |x| x;
  |                 ^
  1. 무엇 — 타입이 안 맞는다. 정수가 와야 하는데 &str 이 왔다.
  2. 어디 — 4번째 줄 same("five"). 첫 note 가 이유를 짚는다. 3번째 줄에서 이 클로저를 정수로 먼저 불렀다.
  3. 고치는 법 — help: 는 없다. 클로저 하나는 타입 하나로만 쓴다. 여러 타입이 필요하면 32편의 제네릭 함수로 만든다.

03 바깥 변수를 고치는 클로저

잡은 변수를 고칠 수도 있다. 이때는 클로저를 담은 변수도 let mut 여야 한다.

rust
fn main() {
    let mut clicks = 0;
    let mut click = || {
        clicks += 1;
        println!("click {clicks}");
    };
    click();
    click();
    click();
    println!("total {clicks}");
}
text
   Compiling closures v0.1.0 (~/projects/closures)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/closures`
click 1
click 2
click 3
total 3

click 은 clicks 를 고칠 수 있게 빌린다(19편의 &mut). 그래서 click 을 쓰는 동안에는 다른 곳에서 clicks 를 읽을 수 없다. 마지막 click() 이 끝난 뒤에 total 을 찍은 것은 그래서다. clicks += 1 은 clicks = clicks + 1 의 줄임이다.

04 실습 — 동작을 넘겨받는 메서드

클로저가 가장 많이 쓰이는 곳은 "어떻게 할지" 를 메서드에 넘길 때다.

rust
fn main() {
    let mut words = vec!["banana", "fig", "apple", "kiwi"];

    words.sort_by_key(|w| w.len());
    println!("{words:?}");

    let min_len = 4;
    words.retain(|w| w.len() >= min_len);
    println!("{words:?}");
}
text
   Compiling closures v0.1.0 (~/projects/closures)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/closures`
["fig", "kiwi", "apple", "banana"]
["kiwi", "apple", "banana"]
  • sort_by_key(|w| w.len()) 은 "글자 수를 기준으로 정렬하라" 다. 28편의 sort() 는 값 자체로만 정렬했다.
  • retain(|w| w.len() >= min_len) 은 클로저가 true 를 돌려주는 것만 남긴다. 바깥 변수 min_len 을 잡아서 썼다.

34편의 wordfreq 에서 같은 횟수의 단어가 알파벳 거꾸로 섰던 것도 클로저로 고칠 수 있다. 다음 편의 반복자는 이런 클로저를 이어 붙여 for 문을 줄인다.

05 자주 묻는 질문

다른 스레드에 클로저를 넘길 때 보이는 move 는 무엇인가?

move |…| … 는 잡은 변수를 빌리지 않고 클로저 안으로 이동시킨다(15편). 클로저가 만든 자리보다 오래 살아야 할 때 쓴다. 43편의 스레드에서 다시 나온다.