tl;dr

  • Rust’s closures are syntax sugar for trait

  • default
    • closure environment on stack (borrow)

    • static dispatch

  • return from function
    • closure environment on heap (use Box)

    • use move to create own stack frame

  • dynamic dispatch
    • use reference

////////////////////////////////////////

// Syntax

fn  plus_one_v1   (x: i32 ) -> i32 { x + 1 }    // function
let plus_one_v2 = |x: i32 | -> i32 { x + 1 };   // closure
let plus_one_v3 = |x: i32 |          x + 1  ;   // closure
let plus_one_v4 = |x      |          x + 1  ;   // closure

////////////////////////////////////////

// dynamic dispatch (reference)

fn f(closure: &Fn(i32) -> i32) -> i32 {
    closure(1)
}

let answer = f(&|x| x + 2);

////////////////////////////////////////

// return closure from function

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;
    Box::new(move |x| x + num)
}

let f = factory();

////////////////////////////////////////

等待的時間總是無聊, 只好打開電腦研究點東西。

今天在 Slack 上聊天時, 聊到了在 Python 可以利用 function 回傳 function 來做一些特別的事, 對於一個 Python 使用者來說這件事很常遇到 (又看看 decorator), 這時突然就連結到 Rust, Rust 裡面也可以定義 local function, 那是否可以像 Python 一樣回傳出去使用?

於是,來翻翻 Rust 的 closure 吧 ~


Rust 不只支援 named function, 還支援匿名函式, 當匿名函式裡有跟當時環境相關的變數時, 就稱為 closure, 以下就描述如何在 Rust 裡運用它。

Syntax

Rust 的 closure 是用兩個 | 把變數夾住, 後面接處理的 code, 可以用 {} 把多個處理包起來 (Rust 的 {} 是 expression)

let plus_one = |x| x + 1;

assert_eq!(2, plus_one(1));

////////////////////////////////////////

let plus_two = |x| {
    let mut result: i32 = x;

    result += 1;
    result += 1;

    result
};

assert_eq!(4, plus_two(2));

////////////////////////////////////////

let plus_one = |x: i32| -> i32 { x + 1 };

assert_eq!(2, plus_one(1));

////////////////////////////////////////

// compare

fn  plus_one_v1   (x: i32 ) -> i32 { x + 1 }
let plus_one_v2 = |x: i32 | -> i32 { x + 1 };
let plus_one_v3 = |x: i32 |          x + 1  ;
let plus_one_v4 = |x      |          x + 1  ;

這邊可以看到 Rust 的 closure 和一般 function 不同, 不需要寫明傳入和回傳的 type (但可以寫明), 這設計是為了方便使用 (一般 function 要寫明是為了 documentation、type inference)。

Closure 和外部變數

先看一段範例 code:

let num = 5;
let plus_num = |x| x + num;

assert_eq!(10, plus_num(5));

這邊的 closure 有用到一個不是參數的變數 num , 他是靠外面的 num 來取得資料, 更明確來說這個 closure 裡的 num 借 (borrow) 了外面 num 的 binding。

如果在那之後又更動了 num 這個變數, 則會在 compile time 時出錯:

let mut num = 5;
let plus_num = |x: i32| x + num;

// error
num = num + 1;

// error: cannot assign to `num` because it is borrowed
//     num = num + 1;
//     ^~~~~~~~~~~~~
// note: borrow of `num` occurs here
//     let plus_num = |x: i32| x + num;
//                 ^~~~~~~~~~~~~~~~

但是可以用 {} 把借用的區域包起來, 這樣超出這段範圍後就會停止借用:

let mut num = 5;

{
    let plus_num = |x: i32| x + num;

} // plus_num goes out of scope, borrow of num ends

num = num + 1;

另外 Rust 的 closure 對於 non-copyable 的變數還會拿走 ownership、move 資源:

let nums = vec![1, 2, 3];

let takes_nums = || nums;

println!("{:?}", nums);


// error: use of moved value: `nums` [E0382]
//     println!("{:?}", nums);
//                     ^~~~
// note: `nums` moved into closure environment here because it has type `collections::vec::Vec<i32>`, which is non-copyable
//     let takes_nums = || nums;
//                     ^~~~~~~

“move” closures

對於不會自動觸發 ownership 轉移的情況, 我們可以使用 move 來強制轉移 (注意的是這邊雖然叫 move,但是表示的是 closure 可以拿到 ownership, 不一定是 resourse 的 move,可能是 copy 一份):

let mut num = 5;

{
    let mut add_num = |x: i32| num += x;

    // modify the original num
    add_num(5);
}

assert_eq!(10, num);
let mut num = 5;

{
    let mut add_num = move |x: i32| num += x;

    // modify a copy version of num
    add_num(5);
}

assert_eq!(5, num);

在沒有使用 move 的情況下, closure 會綁在建立這 closure 的 stack frame 上, move closure 則是 self-contained 的, 這裡也可以發現要回傳一個 non-move closure 是不行的 (因為綁在 function stack frame 上,回傳後會被清掉)。

Closure implementation

Rust closure 的實作和其他語言有點不同, Rust 的 closure 是 trait 的 syntax sugar

在 Rust 裡,用 () 來 call function 這件事是 overloadable 的, 我們可以用 trait 來 overload 這個 operator (有三種 trait 可以 overload):

pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

三種不同的情況,讓我們可以有良好的掌控性:

trait

self

Fn

&self

FnMut

&mut self

FnOnce

self

|| {} 則是這三種情況的 syntax suger, Rust 會為 closure 的外部變數生出 struct, impl 需要的 trait,然後使用它。

closures as arguments

到這邊我們得知 Rust 的 closure 其實就是 trait, 所以如何傳入和回傳 closure 就跟 trait 一樣 (這也表示我們可以選擇 static 或 dynamic dispatch)。

fn call_with_one<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {

    some_closure(1)
}

let answer = call_with_one(|x| x + 2);

assert_eq!(3, answer);

這邊可以看到傳入的 type 為 Fn(i32) -> i32 , Fn 就是個 trait,這邊寫明說會傳入 i32、回傳 i32, 這也就是我們 closure 需要的 type。

這邊一個重點是 Rust 的 closure 可以做 static dispatch, 在許多語言裡,closure 是 heap allocation 並且是 dynamic dispatch, 但是 Rust 可以做 stack allocation 和 static dispatch, 這很常被使用,尤其是在 iterator 那邊常常會傳入 closure 做篩選。

雖然 Rust 支援 static dispatch 的 closure, 但是想要使用 dynamic dispatch 還是可以的:

fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
    some_closure(1)
}

let answer = call_with_one(&|x| x + 2);

assert_eq!(3, answer);

returning closures

第一次失敗的嘗試:

fn factory() -> (Fn(i32) -> Vec<i32>) {
    let vec = vec![1, 2, 3];

    |n| vec.push(n)
}

let f = factory();

let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);

// error: the trait `core::marker::Sized` is not implemented for the type
// `core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
//     f = factory();
//     ^
// note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a
// constant size known at compile-time
//     f = factory();
//     ^
// error: the trait `core::marker::Sized` is not implemented for the type
// `core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
//     factory() -> (Fn(i32) -> Vec<i32>) {
//                  ^~~~~~~~~~~~~~~~~~~~~
// note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a constant size known at compile-time
//     factory() -> (Fn(i32) -> Vec<i32>) {
//                  ^~~~~~~~~~~~~~~~~~~~~

為了要從 function 回傳東西,Rust 需要知道 return type 的大小, 但是 Fn 是一個 trait,它可以包含各種東西、是各種大小, 各種不同的 type 可以實作 Fn , 一個可以知道回傳大小的簡單方式就是用 reference (reference 的大小是已知的)

第二次失敗的嘗試:

fn factory() -> &(Fn(i32) -> Vec<i32>) {
    let vec = vec![1, 2, 3];

    |n| vec.push(n)
}

let f = factory();

let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);

// error: missing lifetime specifier [E0106]
//     fn factory() -> &(Fn(i32) -> i32) {
//                     ^~~~~~~~~~~~~~~~~

這次缺了 lifetime,我們用了 reference, 所以需要給個 lifetime, 這邊沒有參數,狀況很單純,使用 'static

第三次失敗的嘗試:

fn factory() -> &'static (Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);


// error: mismatched types:
// expected `&'static core::ops::Fn(i32) -> i32`,
//     found `[closure <anon>:7:9: 7:20]`
// (expected &-ptr,
//     found closure) [E0308]
//         |x| x + num
//         ^~~~~~~~~~~

compiler 說他拿到的 type 是 [closure <anon>:7:9: 7:20] , 不是我們寫的 &'static Fn(i32) -> i32 , 這是因為每個 closure 都是依照當時的 environment structFn 實作, 這些 type 都是 anonymous 的,所以 Rust 把它視為 closure <anon> 。 至於為何沒有實作 &'static Fn 則是因為 environment 是借來的, 而在這 case 中 environment 是 stack 上的變數, 所以 borrow 的 lifetime 等同於 stack frame 的 lifetime, 如果把 closure 回傳了,function stack frame 就會被清除, closure 裡取到的就是不正確的值。

第四次失敗的嘗試:

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;

    Box::new(|x| x + num)
}
let f = factory();

let answer = f(1);
assert_eq!(6, answer);

// error: `num` does not live long enough
// Box::new(|x| x + num)
//          ^~~~~~~~~~~

這次把 closure 丟到 heap 上了 (用 Box),但是發現 num 還是取到 stack 上的值, 於是又再稍做修改。

成功:

fn factory() -> Box<Fn(i32) -> i32> {
    let num = 5;

    Box::new(move |x| x + num)
}
let f = factory();

let answer = f(1);
assert_eq!(6, answer);

使用 move 來為 closure 建立新的 stack frame來儲存一份使用到的外部變數, 利用 Box 來把 closure 放到 heap 上,如此一來 size 變成已知, 離開原本建立的 stack frame 後也可以使用。

Reference