位元詩人 [Rust] 程式設計教學:函數式程式設計 (Functional Programming)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

函數式程式設計 (functional programming) 是另一種程式設計的模式 (paradigm)。此種模式以函數為主體,撰寫時儘量減少狀態改變,以減少程式的臭蟲。不同程式語言對函數式程式設計的支援程度差異相當大;有些語言整體上即以此模式為主,像是 Lisp、Erlang、OCaml、F# 或是 Haskell 等;有些語言雖然不以此模式為主,但提供部分相關的功能,像是 Perl、Python 或是 Java 8 等。雖然 Rust 官方網站上的資料沒有強調 Rust 和函數式程式設計的關係,Rust 也支援許多函數式程式設計的功能。

Closure

在 Rust 中,函式也可以是物件,如下例:

fn main() {
    let add_one = |x: i32| x + 1;

    assert_eq!(6, add_one(5));
}

在上例中,add_one 是一個函式物件 (function object),透過這個機制,我們可以將函式像物件般存在變數中,之後再呼叫。在 Rust 中,add_one 即為 Closure。在這個範例中 |x: i32| x + 1 是以 Closure 的方式撰寫函數,其中 |x: i32| 的部分是函數參數,而 x + 1 的部分是函數本體;寫 Closure 時,不需要明確指明回傳值,Rust 會自動推斷該有的回傳型別。

註:Rust 的 Closure 型別其實不是函數式程式設計中的閉包 (Closure),比較好的稱呼應該是函式物件。後文中會用範例展示實質的閉包。為了遵守 Rust 的習慣用法,本書仍沿用 Closure 這個詞彙來指稱 Rust 的函式物件。

若 Closure 內容較長,也可改寫成多行的形式,如下:

fn main() {
    let add_one = |x: i32| {
        let mut n = x;
        n += 1;
        n
    };

    assert_eq!(6, add_one(5));
}

或是明確指定回傳型別,如下:

fn main() {
    let add_one = |x: i32| -> i32 {
        let mut n = x;
        n += 1;
        n
    };

    assert_eq!(6, add_one(5));
}

但是 Rust 將函數和 Closure 視為不同的東西,函數不是表達式,而 Closure 是。所以,以下的寫法是錯誤的:

fn main() {
    // fn is not an expression.
    let add_one = fn f(x: i32) -> i32 {
        let mut n = x;
        n += 1;
        n
    };

    assert_eq!(6, add_one(5));
}

由於 Closure 可以作為值,所以,Closure 也可以做為函式的回傳值。如下例:

fn add_one(x: i32) -> Box<Fn(i32) -> i32> {
    Box::new(move |n| n + x)
}

fn main() {
    let f = add_one(5);
    assert_eq!(6, f(1));
}

由於 Closure 的型別不能實體化,而要借助 Box<T> 才能將其實體化。在 Rust 中,Closure 的型別視為一種 trait,和其他的 trait 一樣,本身不能實體化。另外,為了解決所有權的問題,Rust 使用 move 這個關鍵字將變數的所有權移到函式外。

如果我們想要實作有狀態改變的閉包,Closure 的形別要改為 FnMut,如下例:

fn add_one(x: i32) -> Box<FnMut() -> i32> {
    let mut n = x;
    Box::new(move || {
        n += 1;
        n
    })
}

fn main() {
    let mut f = add_one(5);

    assert_eq!(f(), 6);
    assert_eq!(f(), 7);
    assert_eq!(f(), 8);
}

在本例中,add_one 的狀態會存在 f 物件中,每次執行 f 時,其內部的 n 就遞增 1,從外部程式的效果看來,就像是 f 會遞增一樣。

除了作為回傳值為,Closure 還可以作為函式的參數,如下例:

fn my_filter<F, T>(vec: & [T], f: F) -> Vec<T>
    where F: Fn(T) -> bool, T: Copy  {
    let mut v: Vec<T> = Vec::new();

    for i in 0..(vec.len()) {
        if f(vec[i]) == true {
            v.push(vec[i]);
        }
    }

    v
}

fn main() {
    let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let filtered = my_filter(&vec, |x| x % 2 == 0);

    assert_eq!(filtered, vec![2, 4, 6, 8, 10]);
}

在本範例中,my_filter 接受 Closure 為參數,並在函式中呼叫該 Closure,透過該 Closure 實作的條件將 vector 過濾掉不符條件的值。為了讓函式的介面較簡潔,我們這裡使用泛型函式。

高階函式 (higher-order function) 以函式為參數或回傳值,在本節中的 add_one 或是 my_filter 這種函式就稱為高階函式,高階函式是函數式程式設計中相當重要的應用。Rust 實作了許多高階函式,程式設計者不需要再重頭撰寫程式碼。

高階函式

高階函式是「使用函式的函式」,在實作上來說,高階函式以函數為參數或回傳值。透過高階函式,可以用很緊湊的程式碼來撰寫程式。許多的高階函式,使用到串列操作的概念。串列是一種線性容器,如下圖:

串列

對於高階函式的使用者來說,不需要擔心串列的實作。在實務上,高階函數預先寫好相關的串列操作,只要使用者將函式填入參數,即可操作。假設有一個 filter 函數,會過濾掉串列中不符合其條件的元素,示意圖如下:

以 filter 函數過濾元素

(未完待續...)

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。