【在主画面加入捷径】
       
【选择语系】
繁中 简中

Rust 程序设计教学:函数式程序设计 (Functional Programming)

【赞助商连结】

    函数式程序设计 (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 函数过滤元素

    (未完待续…)

    【赞助商连结】