原文標題 : Auto-currying Rust Functions

原文鏈接 : https://peppe.rs/posts/auto-currying_rust_functions/

公衆號 : Rust 碎碎念

本文包含 Rust 中過程宏 (procedural macros) 的介紹示例和通過過程宏柯里化 Rust 函數的指導。 整個庫的源碼可以在這裏 1 找到。 在 crate.io 上也可以找到。

在開始之前,閱讀下面的鏈接會有助於理解 :

  1. 過程宏 (Procedural Macros)2
  2. 柯里化 (Currying)[3]

或者你也可以假裝已經閱讀了上面的文章,因爲我這裏已經包含了基本介紹。

內容

  1. 柯里化 (Currying)
  2. 過程宏
  3. 定義
  4. 改進
  5. 插曲
  • 5.1 依賴
  • 5.2 屬性宏
  • 5.3 函數體
  • 5.4 函數簽名
  • 5.5

  • 調試和測試

  • 注意事項
  • 總結

柯里化 (Currying)

柯里化是指把形如 f(a, b, c) 的函數轉換爲 f(a)(b)(c) 的過程。一個被柯里化的函數只有在它接收到所有的參數之後纔會返回一個具體的值!如果它沒有接受到足夠數量的參數,比如 3 個參數中的 1 個,它仍然返回一個柯里化的函數,該函數會在收到剩餘 2 個參數之後返回。

    curry(f(a, b, c)) = h(a)(b)(c)  

    h(x) = g   <- curried function that takes upto 2 args (g)  
    g(y) = k   <- curried function that takes upto 1 arg (k)  
    k(z) = v   <- a value (v)  

    Keen readers will conclude the following,  
    h(x)(y)(z) = g(y)(z) = k(z) = v  

從數學上來講,如果 f 是一個接收參數爲 xy 的函數 , 使 x ∈ X, y ∈ Y :

    f: (X × Y) -> Z  

其中 × 表示集合 X 和集合 Y 進行笛卡爾乘積,柯里化的 f(這裏標記爲 h) 表示爲 :

    h: X -> (Y -> Z)  

過程宏 (Procedural Macros)

它以代碼作爲輸入,以修改後的代碼作爲輸出。Rust 有三種過程宏 :

  1. 函數風格的宏
  2. 繼承宏 : #[derive(...)], 常被用於爲結構體 / 枚舉自動地實現 trait
  3. 屬性宏 : #[test], 通常附着在函數上

我們將會使用屬性宏來對 Rust 函數進行柯里化,即變成通過 function(arg1)(arg2) 這種方式來調用的函數。

定義 (Definitions)

作爲受人尊敬的程序員,我們定義了輸入以及過程宏的輸出。我們從一個比較特別的函數開始 :

    fn add(x: u32, y: u32, z: u32) -> u32 {  
      return x + y + z;  
    }  

我們的輸出會是什麼樣子?理想情況下我們的過程宏應該生成什麼?如果我們正確理解了柯里化,我們應該接收一個參數並且返回一個接收一個參數的函數,然後這個函數返回一個 ... 你應該懂了,就像下面這樣 :

    fn add_curried1(x: u32) -> ? {  
      return fn add_curried2 (y: u32) -> ? {  
        return fn add_curried3 (z: u32) -> u32 {  
          return x + y + z;  
        }  
      }  
    }  

有幾件事需要注意 :

返回類型 (Return types)

我們用 ? 替代了返回類型。讓我們嘗試修正它。add_curried3 返回了一個值,所以這裏返回 u32 是準確的。add_curried2 返回了 add_curried3, 所以 add_curried3 的類型是什麼?它是一個函數,以 u32 作爲參數並返回一個 u32, 所以 fn(u32)->u32 正確麼?不, 接下來我將會解釋原因,但是現在,我們將會利用 Fntrait, 讓我們的返回類型 impl Fn(u32)->u32。這基本上是在告訴編譯器我們將會返回類似函數的東西, 行爲就像 Fn, 酷!

如果你已經跟上來了,你應該就能猜到 add_curried1 的返回類型是

    impl Fn(u32) -> (impl Fn(u32) -> u32)  

我們可以丟掉外面的括號,應該 -> 是右關聯的 :

    impl Fn(u32) -> impl Fn(u32) -> u32  

訪問上下文環境 (Accessing environment)

函數是不能訪問周圍上下文環境的。我們的方案無法生效。add_curried3 嘗試訪問 x,這是不被允許的 ! 但是閉包 [4] 可以。如果我們返回一個閉包,我們返回類型就必須 impl Fn 而不是 fnFntrait 和函數指針的區別已經超出本文範圍,不再討論。

改進 (Refinement)

掌握了上述知識,我們改進期望的輸出去,這一次,我們使用閉包 :

    fn add(x: u32) -> impl Fn(u32) -> impl Fn(u32) -> u32 {  
      return move |y| move |z| x + y + z;  
    }  

但是這樣無法編譯通過,編譯器報出下面的錯誤信息 :

    error[E0562]: `impl Trait` not allowed outside of function  
    and inherent method return types  
      --> hide/main.rs:17:37  
       |  
       | fn add(x: u32) -> impl Fn(u32) -> impl Fn(u32) -> u32  
       |  

只能在函數內部返回一個 impl Fn。我們現在是從另一個返回裏返回。至少,這是我從錯誤信息裏獲得的最多的理解。
我們不得不通過欺騙編譯器來修正這個問題;通過使用類型別名 (type aliase) 和 nightly 版本中的一個特性 [5]。

    #![feature(type_alias_impl_trait)]  // allows us to use `impl Fn` in type aliases!  

    type T0 = u32;                 // the return value when zero args are to be applied  
    type T1 = impl Fn(u32) -> T0;  // the return value when one arg is to be applied  
    type T2 = impl Fn(u32) -> T1;  // the return value when two args are to be applied  

    fn add(x: u32) -> T2 {  
      return move |y| move |z| x + y + z;  
    }  

把上面的代碼丟到 cargo 工程裏,然後調用 add(4)(5)(6), 祈求成功,然後運行 cargo +nightly run。你應該看到一個 15,除非你忘記打印它。

插曲 (The In-Betweens)

讓我們寫一些能夠把函數進行柯里化的神奇的片段。使用 cargo new --lib currying 初始化你的工作空間。過程宏 crates 是隻把自身進行導出的庫。在 crate 根目錄下添加一個 tests 目錄。你的目錄看起來應該像下面這樣 :

    .  
    ├── Cargo.toml  
    ├── hide  
    │   └── lib.rs  
    └── tests  
        └── smoke.rs  

依賴 (Dependencies)

我們總共將會使用 3 個外部的 crate:

  1. proc_macro2[6]
  2. syn[7]
  3. quote[8]

這裏是 Cargo.toml 的示例 :

    # Cargo.toml  

    [dependencies]  
    proc-macro2 = "1.0.9"  
    quote = "1.0"  

    [dependencies.syn]  
    version = "1.0"  
    features = ["full"]  

    [lib]  
    proc-macro = true  # this is important!  

我們將會使用一個外部的 proc-macro2 create, 和一個內部的 proc-macrocrate。對此無需困惑。

屬性宏 (The attribute macro)

把下面這些丟進 hide/lib.rs, 準備開始。

    // hide/lib.rs  

    use proc_macro::TokenStream;  // 1  
    use quote::quote;  
    use syn::{parse_macro_input, ItemFn};  

    #[proc_macro_attribute]   // 2  
    pub fn curry(_attr: TokenStream, item: TokenStream) -> TokenStream {  
      let parsed = parse_macro_input!(item as ItemFn);  // 3  
      generate_curry(parsed).into()  // 4  
    }  

    fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {}  

1. 導入 (Imports)

一個 Tokenstream 包含 (希望是有效的)Rust 代碼,這是我們輸入和輸出的類型。注意,我們是從 proc_macro 而不是 proc_macro2 導出這個類型。

quotecrate 裏的 quote! 是一個能夠讓我們快速生成 TokenStream 的宏。和 LISP 的 quote 過程很像,你可以使用 quote! 宏來進行符號轉換。

syncrate 中的 ItemFn 含有被解析的一個 Rust 函數裏的 TokenStreamparse_macro_input!syn 提供的一個很有用的宏。

2. 單獨導出 (The lone export)

使用 #[proc_macro_attribute] 來標註我們的 crate 中的 pub 部分。這告訴 rustccurry 是一個過程宏,並且我們能夠在其他的 crate 中通過 #[crate_name::curry] 來使用它。注意 curry 函數的簽名。_attr 是代表屬性自身的 TokenStream,item 表示我們要把宏改造成的事物,在這個例子中是個函數 (比如 add)。返回值是被修改後的 TokenStream, 這將會包含我們對 add 進行柯里化的版本。

3. 輔助宏 (The helper macro)

一個 TokenStream 難以完成工作,這也是爲什麼我們要有一個能夠爲 Rust 符號提供表達類型的 syncrate。一個 RArrow 結構體表示一個函數後面的返回箭頭符號,諸如此類。其中一個類型就是 ItemFn,它表示一整個 Rust 函數。parse_macro_input! 自動把宏的輸入放入 ItemFn

4. 返回 TokenStreams(Returning TokenStreams)

我們還沒有完善 generate_curry, 但是我們能看到它返回一個 proc_macro2::TokenStream 而不是 proc_macro::TokenStream, 所以我們使用一個 .into() 來進行轉換。

讓我們繼續完善 generate_curry, 我會建議你打開 syn::ItemFn[9] 和 syn::Signature[10] 的文檔。

    // hide/lib.rs  

    fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {  
      let fn_body = parsed.block;      // function body  
      let sig = parsed.sig;            // function signature  
      let vis = parsed.vis;            // visibility, pub or not  
      let fn_name = sig.ident;         // function name/identifier  
      let fn_args = sig.inputs;        // comma separated args  
      let fn_return_type = sig.output; // return type  
    }  

我們簡單地導出這個函數的片段, 我們將會重用原始函數的可見性和名字。來看看 syn::Signature 可以告訴我們一個函數的什麼有關信息 :

                           .-- syn::Ident (ident)  
                          /  
                     fn add(x: u32, y: u32) -> u32  
      (fn_token)      /     ~~~~~~~,~~~~~~  ~~~~~~  
    syn::token::Fn --'            /               \       (output)  
                                 '                 `- syn::ReturnType  
                 Punctuated (inputs)  

分析得差不多了,讓我們寫下第一段 Rust 代碼。

函數體 (Function Body)

回想一下,柯里化的 add 函數應該是像下面這樣 :

    return move |y| move |z| x + y + z;  

更通用一點 :

    return move |arg2| move |arg3| ... |argN|   

generate_curry 函數裏,我們已經有了由 fn_body 提供的函數體。接下來要做的就是把 move |arg2| move |arg3| ... 這些東西添加進去,要完成這個目標,我們需要導出參數的標識 (文檔 :Punctuated[11],FnArg[12],PatType[13]):

    // hide/lib.rs  
    use syn::punctuated::Punctuated;  
    use syn::{parse_macro_input, FnArg, Pat, ItemFn, Block};  

    fn extract_arg_idents(fn_args: Punctuated) -> Vec<Box> {   
      return fn_args.into_iter().map(extract_arg_pat).collect::<Vec<_>>();  
    }  

好吧,所以我們正在遍歷函數的參數 (Punctuated 是一個你可以進行遍歷的集合) 並且把 extract_arg_pat 映射到每一項。extract_arg_pat 是什麼?

    // hide/lib.rs  

    fn extract_arg_pat(a: FnArg) -> Box {  
      match a {  
        FnArg::Typed(p) => p.pat,  
       _=> panic!("Not supported on types with `self`!"),  
      }  
    }  

或許你已經猜到,FnArg 是一個枚舉類型。Typed 變量包含一些形式爲 name:type 的參數和其他的變量,Receiver 指向 self 類型。現在先忽略這些,保持簡單。

每個 FnArg::Typed 值包含一個 pat,從本質上講,pat 是參數的名字。參數的類型可以通過 p.ty 來獲取 (我們會在後面用到)。

瞭解上述內容後,我們應該能夠寫出函數體的代碼生成:

    // hide/lib.rs  

    fn generate_body(fn_args: &[Box], body: Box) -> proc_macro2::TokenStream {  
      quote! {  
        return #( move |#fn_args|  )* #body  
      }  
    }  

這些語法令人感到恐怖。容我來解釋一下。quote!{ ... } 返回一個 proc_macro2::TokenStream,如果我們寫 quote!{ let x = 1 + 2; },這不會創建一個值爲 3 的變量 x, 它只會產生關於這個表達式的一個符號流。

# 能夠插入變量。#body 將會在當前域中查找 body, 獲取它的值, 然後在返回的 TokenStream 中將其插入。有點像 LISP 中的準引用。

那麼 #( move |#fn_args| )* 是什麼呢?這是重複。quote 遍歷 fn_args,然後在每一個參數前面加上一個 move, 然後在參數兩邊放上豎線 |

讓我們先測試一段代碼生成!把 generate_curry 修改成下面這樣 :

    // hide/lib.rs  

     fn generate_curry(parsed: ItemFn) -> TokenStream {  
       let fn_body = parsed.block;  
       let sig = parsed.sig;  
       let vis = parsed.vis;  
       let fn_name = sig.ident;  
       let fn_args = sig.inputs;  
       let fn_return_type = sig.output;  

    +  let arg_idents = extract_arg_idents(fn_args.clone());  
    +  let first_ident = &arg;_idents.first().unwrap();  

    +  // remember, our curried body starts with the second argument!  
    +  let curried_body = generate_body(&arg;_idents[1..], fn_body.clone());  
    +  println!("{}", curried_body);  

       return TokenStream::new();  
     }  

tests/ 目錄下加上測試 :

    // tests/smoke.rs  

    #[currying::curry]  
    fn add(x: u32, y: u32, z: u32) -> u32 {  
      x + y + z  
    }  

    #[test]  
    fn works() {  
      assert!(true);  
    }  

你應該能在 cargo test 的輸出裏找到一些類似下面這樣的內容 :

    return move | y | move | z | { x + y + z }  

極好的 println! 調試 !

函數簽名 (Function signature)

這一部分將要深入宏更復雜的部分-生成類型別名和函數簽名。到了這部分結尾,我們就能擁有一個可以完整工作的自動柯里化的宏!

回顧一下,對於我們的 add 函數,生成的類型別名應該看起來是什麼樣子:

    type T0 = u32;  
    type T1 = impl Fn(u32) -> T0;  
    type T2 = impl Fn(u32) -> T1;  

更通用一點兒 :

    type T0 = <return type>;  
    type T1 = impl Fn(<type of arg N>) -> T0;  
    type T2 = impl Fn(<type of arg N - 1>) -> T1;  
    .  
    .  
    .  
    type T(N-1) = impl Fn(<type of arg 2>) -> T(N-2);  

要生成上面的內容,我們需要下面這些類型 :

  1. 所有的輸入 (參數)
  2. 輸出 (返回類型)

要獲取所有輸入的類型,我們可以簡單地複用之前寫過的獲取變量名的代碼片段 (doc: Type[14])

    // hide/lib.rs  

    use syn::{parse_macro_input, Block, FnArg, ItemFn, Pat, ReturnType, Type};  

    fn extract_type(a: FnArg) -> Box {  
      match a {  
        FnArg::Typed(p) => p.ty,  // notice `ty` instead of `pat`  
         _=> panic!("Not supported on types with `self`!"),  
      }  
    }  

    fn extract_arg_types(fn_args: Punctuated) -> Vec<Box> {  
      return fn_args.into_iter().map(extract_type).collect::<Vec<_>>();  

    }  

一個好的讀者應該已經看過 syn::Signature 結構體的輸出成員的相關文檔。它的類型是 syn::ReturnType,所以這裏是沒有提取操作對麼?事實上,這裏真的有些事是需要我們確認的:

  1. 我們需要確認函數返回值 ! 在這種情況下,一個沒有返回的函數是無意義的,我會在最後的注意部分解釋原因。
  2. 一個 ReturnType 會關閉返回的箭頭,我們需要避免這種情況。回顧 :
    type T0 = u32  
    // and not  
    type T0 = -> u32  

下面是處理導出返回類型的代碼段 (doc:syn::ReturnType[15]):

    // hide/lib.rs  

    fn extract_return_type(a: ReturnType) -> Box {  
      match a {  
        ReturnType::Type(_, p) => p,  
       _=> panic!("Not supported on functions without return types!"),  
      }  
    }  

你或許已經注意到,我們正在廣泛使用 panic! 宏。很好,這是因爲當收到一個不滿足條件的 TokenStream 時執行退出是一個很好的主意。

類型已經準備好了,現在我們可以繼續生產類型別名:

    // hide/lib.rs  

    use quote::{quote, format_ident};  

    fn generate_type_aliases(  
      fn_arg_types: &[Box],  
      fn_return_type: Box,  
      fn_name: &syn;::Ident,  
    ) -> Vec {    // 1  

      let type_t0 = format_ident!("_{}_T0", fn_name);    // 2  
      let mut type_aliases = vec![quote! { type #type_t0 = #fn_return_type  }];  

      // 3  
      for (i, t) in (1..).zip(fn_arg_types.into_iter().rev()) {  
        let p = format_ident!("_{}_{}", fn_name, format!("T{}", i - 1));  
        let n = format_ident!("_{}_{}", fn_name, format!("T{}", i));  

        type_aliases.push(quote! {  
            type #n = impl Fn(#t) -> #p  
        });  
      }  

      return type_aliases;  
    }  

1. 返回值

我們正在返回一個 Vec, 也就是,一個 TokenStream 列表,其中每一項都一個類型別名。

2. 格式化標識符

我對此有一些解釋。顯然,我們正在嘗試寫第一個類型別名,然後用 T0 初始化我們的 TokenStream vector,因爲它和其他的不一樣。

    type T0 = something  
    // the others are of the form  
    type Tr = impl Fn(something) -> something  

format_ident!format! 相似。它返回一個 syn::Ident 而不是一個格式化後的字符串。因此, type_t0 實際上是一個標識符,在我們的例子中,add 函數的標識符就是 _add_T0。爲什麼這個格式化很重要?命名空間。

看下面的代碼,我們有兩個函數,addsubtract, 我們希望通過宏對這兩個函數進行柯里化。

    #[curry]  
    fn add(...) -> u32 { ... }  

    #[curry]  
    fn sub(...) -> u32 { ... }  

下面的功能是一樣的,但是對宏進行了展開 :

    type T0 = u32;  
    type T1 = impl Fn(u32) -> T0;  
    fn add( ... ) -> T1 { ... }  

    type T0 = u32;  
    type T1 = impl Fn(u32) -> T0;  
    fn sub( ... ) -> T1 { ... }  

我們最終擁有對 T0 的兩個定義。現在,我們進行少量的 format_ident!

    type_add_T0 = u32;  
    type_add_T1 = impl Fn(u32) ->_add_T0;  
    fn add( ... ) ->_add_T1 { ... }  

    type_sub_T0 = u32;  
    type_sub_T1 = impl Fn(u32) ->_sub_T0;  
    fn sub( ... ) ->_sub_T1 { ... }  

類型別名不再互相沖突了。記得要從 quotecrate 裏導出 format_ident

3. The TokenStream Vector

我們以相反的順序遍歷我們的類型 (T0 是最後的返回, T1 是倒數第二個,以此類推),使用 zip 對每一次迭代賦一個數字,使用 format_ident 生成類型別名,使用 quote 和變量插入把 TokenStream 送入 (vector)。

如果你想知道爲什麼我們使用 (1..).zip() 而不是 .enumerate(),這是因爲我們想要從 1 開始而不是從 0 開始 (我們已經用過 T0 了)。

彙總 (Getting it together)

我承諾過到最後一部分,我們將會擁有一個能夠完成工作的宏。我撒謊了,我們不得不把所有的東西彙總到 generate_curry 函數裏。

    // hide/lib.rs  

     fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {  
       let fn_body = parsed.block;  
       let sig = parsed.sig;  
       let vis = parsed.vis;  
       let fn_name = sig.ident;  
       let fn_args = sig.inputs;  
       let fn_return_type = sig.output;  

       let arg_idents = extract_arg_idents(fn_args.clone());  
       let first_ident = &arg;_idents.first().unwrap();  
       let curried_body = generate_body(&arg;_idents[1..], fn_body.clone());  

    +  let arg_types = extract_arg_types(fn_args.clone());  
    +  let first_type = &arg;_types.first().unwrap();  
    +  let type_aliases = generate_type_aliases(  
    +      &arg;_types[1..],  
    +      extract_return_type(fn_return_type),  
    +      &fn;_name,  
    +  );  

    +  let return_type = format_ident!("_{}_{}", &fn;_name, format!("T{}", type_aliases.len() - 1));  

    +  return quote! {  
    +      #(#type_aliases);* ;  
    +      #vis fn #fn_name (#first_ident: #first_type) -> #return_type {  
    +          #curried_body ;  
    +      }  
    +  };  
     }  

大部分增加的代碼都很容易理解,我會和你一起看看返回語句。我們返回一個 quote!{ ... }, 也就是一個 proc_macro2::TokenStream。我們正在遍歷 type_aliases 變量,你應該可以想到,這是一個 vec。你可能注意到在 * 前面不起眼的分號。這是在告訴 quote, 要插入一項,然後是一個分號,然後插入下一項,然後是下一個分號,以此類推。這個分號是一個分隔符。我們需要手動插入另一個分號在全部結束的時候,quote 不會在迭代結束後插入一個分隔符。

我們保持原來函數的可見性和名字。我們的柯里化後的函數接收參數,就像原來的函數接收的第一個參數一樣。柯里化函數的返回類型是我們創建的最後的類型別名。如果你回想一下我們手工進行柯里化的 add 函數,我們返回了 T2,事實上 T2 就是我們創建的最後的類型別名。

我保證,到這裏,你一定渴望測試一下輸出結果,但是在那之前,讓我爲你介紹一些調試過程宏代碼的好方法。

調試和測試 (Debugging and Testing)

通過下面的命令安裝 cargo-expand:

    cargo install cargo-expand  

cargo-expand 是一個簡潔的工具,它能夠把你的宏在原地展開,讓你看到生成的代碼!例如 :

    # create a bin package hello  
    $ cargo new hello  

    # view the expansion of the println! macro  
    $ cargo expand  

    #![feature(prelude_import)]  
    #[prelude_import]  
    use std::prelude::v1::*;  
    #[macro_use]  
    extern crate std;  
    fn main() {  
      {  
        ::std::io::_print(::core::fmt::Arguments::new_v1(  
            &["Hello, world!\n"],  
            &match; () {  
                () => [],  
            },  
          ));  
      };  
    }  

不使用 cargo-expand 來寫過程宏就像駕駛沒有後視鏡的貨車。它給你一雙眼睛看到背後發生的事情。

現在,你的宏將不會總是編譯,你只要收到蜜蜂電影劇本當做錯誤 (譯者注 : 這句話沒看懂)。cargo-expand 將不會在這樣的情況下工作。我建議打印出你的變量以便於檢查。TokenStream 實現了 DisplayDebug。我們不必總是令人尊重的程序員。只要把它打印出來。

現在讓我們開始測試:

    // tests/smoke.rs  

    #![feature(type_alias_impl_trait)]  

    #[crate_name::curry]  
    fn add(x: u32, y: u32, z: u32) -> u32 {  
       x + y + z  
    }  

    #[test]  
    fn works() {  
      assert_eq!(15, add(4)(5)(6));  
    }  

運行 cargo +nightly test。你應該能夠看到一條令人愉快的消息 :

    running 1 test  
    test tests::works ... ok  

通過 cargo +nightly expand --tests smoke 看一看我們的柯里化宏的展開:

    type_add_T0 = u32;  
    type_add_T1 = impl Fn(u32) ->_add_T0;  
    type_add_T2 = impl Fn(u32) ->_add_T1;  
    fn add(x: u32) ->_add_T2 {  
      return (move |y| {  
        move |z| {  
          return x + y + z;  
        }  
      });  
    }  

    // a bunch of other stuff generated by #[test] and assert_eq!  

這是個更復雜的例子,它生產前十個自然數的十倍數 :

    #[curry]  
    fn product(x: u32, y: u32) -> u32 {  
      x * y  
    }  

    fn multiples() -> Vec<Vec<u32>>{  
      let v = (1..=10).map(product);  
      return (1..=10)  
          .map(|x| v.clone().map(|f| f(x)).collect())  
          .collect();  
    }  

注意 (Notes)

我沒有解釋爲什麼我們在閉包裏使用 move |arg|。這是因爲我們想要獲取提供給我們的變量的所有權。看看下面的例子 :

    let v = add(5);  
    let g;  
    {  
      let x = 5;  
      g = v(x);  
    }  
    println!("{}", g(2));  

變量 xg 能夠返回一個具體值之前就離開了作用域。如果我們通過把它 move 到我們的閉包中來獲取 x 的所有權,這就能達到我們預期的工作。事實上,rustc 理解這個,並且強制你使用 move

move 的使用就解釋了爲什麼一個沒有返回的柯里化函數是無意義的。我們傳遞給柯里化函數每個變量都被移動進了它的本地作用域。使用這個變量不會引起作用域之外的變化。返回是我們唯一的和函數體外部進行交互的方式。

總結 (Conclusion)

柯里化可能不是總是有用。Rust 中的柯里化函數稍顯笨重,因爲標準庫沒有圍繞柯里化來構建。如果你喜歡柯里化帶來的可能性,可以考慮看一下 Haskell 或者 Scheme。

我最初的目的是想寫一篇簡短的博客,但是現在看來寫的有點長了。
或許我應該叫它宏博客 :)

參考資料

1

這裏 :https://github.com/nerdypepper/cutlass

2

過程宏 (Procedural Macros):https://doc.rust-lang.org/reference/procedural-macros.html

[3]

柯里化 (Currying):https://en.wikipedia.org/wiki/Currying

[4]

閉包 :https://doc.rust-lang.org/book/ch13-01-closures.html

[5]

特性 :https://peppe.rs/posts/auto-currying_rust_functions/#fn2

[6]

proc_macro2:https://docs.rs/proc-macro2/1.0.12/proc_macro2/

[7]

syn:https://docs.rs/syn/1.0.18/syn/index.html

[8]

quote:https://docs.rs/quote/1.0.4/quote/index.html

[9]

syn::ItemFn:https://docs.rs/syn/1.0.19/syn/struct.ItemFn.html

[10]

syn::Signature:https://docs.rs/syn/1.0.19/syn/struct.Signature.html

[11]

Punctuated:https://docs.rs/syn/1.0.18/syn/punctuated/struct.Punctuated.html

[12]

FnArg:https://docs.rs/syn/1.0.18/syn/enum.FnArg.html

[13]

PatType:https://docs.rs/syn/1.0.18/syn/struct.PatType.html

[14]

Type:https://docs.rs/syn/1.0.18/syn/enum.Type.html

[15]

syn::ReturnType:https://docs.rs/syn/1.0.19/syn/enum.ReturnType.html

歡迎關注微信公衆號 : Rust 碎碎念

【翻譯】自動柯里化 Rust 函數

來源鏈接:mp.weixin.qq.com