2013年1月9日星期三

Rust中的匿名函数与闭包

Rust中的匿名函数与闭包(Closures)

根据Rust 0.6的tutorial整理。

“闭包”这个概念来自于抽象代数,是指一个元素的集合在某个运算之下封闭,即将该运算应用于这个集合的元素,所得到的结果仍然是该集合的元素。不幸的是,Lisp社区将“闭包”用来表示另一种含义:闭包是一种为表示带有自由变量的过程而用的实现技术。简单说就是过程中可以使用自由变量(自由变量是相对于约束变量而言,约束变量即过程的形参,以及过程中定义的变量),严格地讲是指能捕获上下文环境。在编程语言中,“闭包”这个概念很不幸指的是Lisp社区的概念。

先使用“闭包”概念的源头——Lisp(Scheme)——来说明简单说明闭包。
我们定义一个函数make-add-n。该函数接受一个数值参数n,返回一个匿名函数。 该匿名函数接受一个数值参数i,返回i+n的值。

(define (make-add-n n)
  (lambda (i)
    (+ i n)))

lambda中的n对于lambda来讲就是自由变量。我们使用make-add-n生成一个add-3函数:

(define add-3 (make-add-n 3))

> (add-3 5)
8

可以看出,make-add-n的参数n在make-add-n调用结束后就已经离开了作用域,不存在了。但是add-3依然可以访问n(这里n=3),也就是说add-3(或者说make-add-n中的lambda)捕获了n(n=3)。

下面回到Rust。在Rust中,具名函数(用fn定义的那些给出了名字的函数)是不具备闭包能
力的,Rust中的闭包由匿名函数来完成。

1 Rust中的匿名函数

我们由一个例子引出匿名函数。下例中,我们定义一个匿名函数,该匿名函数接受一个参数,并将参数的值打印出来。然后我们将该匿名函数赋值给变量closure。
let closure = |arg| io::println(fmt!("arg=%d", arg));
然后closure就可以像函数一样调用:

    closure(10);

从上例中可以看出,Rust的匿名函数由双竖线| |引出。双竖线中是匿名函数的参数列表,后面跟着匿名函数的表达式。匿名函数的参数类型以及返回值类型通常可以忽略,交由编译器推断。有时编译器推断不出来,则需要我们加上类型说明:

let square = |x: int| -> uint { x * x as uint };
上面的例子中,匿名函数所做的操作都是很简单的,如果要做一点复杂的事情怎么办?比如要用if判断?用下面这个例子说明:
 
    let abs = |x: int| {
        if x > 0 {
            x
        } else { -x }
    };

到目前为止一切似乎都很简单,哦,等等,复杂的来了——“闭包”。

闭包本身不复杂,那个scheme的例子基本说明了闭包是怎么一回事。不过呢,到了Rust中,事情变得有趣了很多。我们还记得Rust的内存模型吧?Rust中的数据有三处存储地可选:栈上、托管堆上、以及交换堆上。所以呢,Rust中有三种闭包,你没听错,是三种。分别对应于三种存储地:stack closure、managed closure、owned closure
:tutorial中是把closure和匿名函数合在一起讲的,重点在强调closure特性,但我觉得分开说好一些,匿名函数可以使用闭包,也可以不使用闭包。不使用闭包特性的匿名函数照样很有用。使用stack closure、managed closure、owned closure术语是重点强调这几种匿名函数对于捕获的变量的要求,所以我也沿用这几个术语。

2 Stack closures

Stack closures的类型为&fn,可以直接访问它上下文环境中的局部变量:
let mut max = 0;
[1, 2, 3].map(|x| if *x > max { max = *x });
Stack closures非常高效,因为它们的环境是分配在栈上,同时通过指针访问捕获的变量(不用拷贝)。为了保证stack closures的生命周期不会比它们捕获的变量还长,stack closures不能作为第一等级函数使用。就是说:它们不能存储在数据结构中,也不能当作函数的返回值;它们通常用作某些高阶函数的参数,如上例中作为map的参数。除了这些限制,stack closure在Rust中被普遍使用(匿名简短小函数)。

 

3 Managed closures

Managed closures对应于managed boxes,即在托管堆上存储的closures。因此生存周期没有stack closures那样的限制。类型为@fn。是第一等级函数,可以存储,可以被当作返回值返回。当然,继承自managed boxes的限制,managed closures无法跨越tasks边界。

另外,managed closure不直接访问它的上下文环境,而是将它捕获的值拷贝为私有数据结构。因此它不能对这些捕获的变量进行赋值,也无法检测到外部值得变化。

来看一个例子:
fn mk_appender(suffix: ~str) -> @fn(~str) -> ~str {
    // The compiler knows that we intend this closure to be of type @fn
    return |s| s + suffix;
}

fn main() {
    let shout = mk_appender(~"!");
    io::println(shout(~"hey ho, let's go"));
}

4 Owned closures

Owned closures,写作~fn,类似于owned boxes,存储于交换堆,因此可以在tasks之间传递。和managed boxes类似,也拷贝它们所捕获的值,不同的是,owned closures拥有这些值的所有权,别的代码无法访问这些捕获的值。Owned closures用于并发代码,特别是生成tasks。

 

5 匿名函数的通用性

 Rust的匿名函数有个很便利的子类型化特性:你可以将任何类型的匿名函数(只要参数和返回值类型匹配)传递给接受fn()为参数的函数。因此,如果写了一个高阶函数,仅仅调用其的函数参数,而不对参数作别的事,那么应该将其参数声明为fn()类型。这样的话,调用者就可以传入任意类型的函数(包括具名函数)。
fn call_twice(f: fn()) { f(); f(); }
let closure = || { "I'm a closure, and it doesn't matter what type I am"; };
fn function() { "I'm a normal function"; }
call_twice(closure);
call_twice(function); 

6 Do 记法

do记法提供了将高阶函数(以函数为参数的函数)转换为控制结构的方法。
例如,each函数对一个vector的元素进行迭代,以一个vector和一个函数为参数,将函数应用于vector的每个元素上:
fn each(v: &[int], op: fn(v: &int)) {
   let mut n = 0;
   while n < v.len() {
       op(&v[n]);
       n += 1;
   }
}
调用each时,如果我们使用匿名函数作为参数,那么我们可以写成一种很好看的形式:
each([1, 2, 3], |n| {
    do_some_work(n);
});
这种形式很常用,因此Rust提供了一个特殊的函数调用形式,使得上面的调用看起来更像内置的控制结构:
do each([1, 2, 3]) |n| {
    do_some_work(n);
}
上面的方式以关键字do开头,不将最后的匿名函数参数写入括号内,而是写在括号外,这样看起来更像一个常见的代码块(很像Ruby的Block)。
 
使用do来调用task::spawn创建task很方便。spawn函数的类型签名为spawn(fn: ~fn())。也就是说,spawn是一个函数,接受一个无参数的ownend closure为参数。
use task::spawn;

do spawn() || {
    debug!("I'm a task, whatever");
}
上面的括号和竖线中什么都没有,所以可以省略:
do spawn {
   debug!("Kablam!");
} 

7 For 循环

Rust中最常用的进行迭代的方式是使用for循环。和do类似,for也有着将closures描述成控制结构的漂亮语法。另外,在for循环中,break、loop、以及return的作用和它们在while和loop中相同。

还是用each函数这个例子,这次我们当函数参数返回false时break:
fn each(v: &[int], op: fn(v: &int) -> bool) {
   let mut n = 0;
   while n < v.len() {
       if !op(&v[n]) {
           break;
       }
       n += 1;
   }
}
使用该函数对vector进行迭代:
each([2, 4, 8, 5, 16], |n| {
    if *n % 2 != 0 {
        println("found odd number!");
        false
    } else { true }
});
有了for,each这样的函数就可以更像内建循环结构。在for循环中调用each,不需要通过返回false来跳出循环,直接使用break即可。如果需要调制下一次迭代开头,使用loop。
for each([2, 4, 8, 5, 16]) |n| {
    if *n % 2 != 0 {
        println("found odd number!");
        break;
    }
}
另外,你还可以在作为for循环体的block中使用return,这在closures中通常是不允许的,这时return将从使用for的函数中返回,而不仅仅是循环体。
fn contains(v: &[int], elt: int) -> bool {
    for each(v) |x| {
        if (*x == elt) { return true; }
    }
    false
}
注意,each将每个值通过borrowed pointer传递,所以每次使用值时需要解引用。我们可以使用Rust的参数模式匹配来直接获取值,而不是指针:
    for each(v) |&x| {
        if (x == elt) { return true; }
    }
for记法只能用于stack closures。

没有评论:

发表评论