Dart中的函数式编程
JavaScript的一个特点是它支持函数式编程。因为Dart的目标是让人感觉熟悉,现在让我们看看在Dart语言中函数式编程是什么样的。
我们先从一个传统的例子开始,计算斐波纳契数列。在JavaScript中,大概像下面这样写:
function fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
探索一个语言的函数式编程特性,斐波纳契数列是个很好的例子。不仅因为它是一个函数,也因为它的递归性质可以展示函数的调用。
我不想纠缠于描述递归或者这个函数的细节。相反,让我们关注如何在JavaScript中使用这个函数。
fib(1) // => 1
fib(3) // => 2
fib(10) // => 55
看得出JavaScript函数足够简单。首先是function关键字,然后是函数名,跟着是圆括号中的参数列表,最后是描述函数体的代码块。
那么,等价的Dart语言版本是什么样呢?
// Dart
fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
等一下,这和JavaScript的版本有什么不同吗?
function fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
细心的读者会注意到,Dart版本缺少function关键字。除了这一点,两个函数是完全相同的,调用方式也一样。
fib(1); // => 1
fib(5); // => 5
fib(10); // => 55
如果没有其他区别,可以看出Dart语言的设计者确实让这门语言让人感觉很熟悉。
匿名函数
有经验的JavaScript程序员非常精通于使用匿名函数。因为在JavaScript中函数是顶级概念,函数可以在JavaScript中任意传递。甚至某些框架成了回调函数的地域,但是撇开审美不说,没有人会否认匿名函数是JavaScript中的一个重要部分。那么,在Dart中也一定是这样的,对吗?
在JavaScript中,匿名函数省略了函数名,仅使用function关键字。
function(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
我们已经看到JavaScript和Dart的函数仅有的差异是后者没有function关键字。事实证明,这也是二者在匿名函数上仅有的差异。
(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
乍一看,这看起来很奇怪,感觉光秃秃的。但是,这仅仅是从JavaScript的角度来看。Ruby中有比较类似的lambda和Proc。
{ |i| $stderr.puts i }
认真地考虑一下,在JavaScript中function关键字真正起了什么作用?下意识的反应是,它有助于标识出匿名函数,但在实践中,这仅仅是一个干扰。
考虑下面这个显示斐波纳契数值的代码:
var list = [1, 5, 8, 10];
list.forEach(function(i) {fib_printer(i)});
function fib_printer(i) {
console.log(“Fib(“ + i + ”): ” + fib(i));
}
function fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
function关键字对代码的可读性有帮助还是有阻碍?显然,这使情况变得更糟,尤其是在foreach()调用的内部。
让我们考虑以下等价的Dart代码。
var list = [1, 5, 8, 10];
list.forEach((i) {fib_printer(i);});
fib_printer(i) {
print(“Fib($i): ${fib(i)}”);
}
fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
我们所做的只是删除了function关键字,但是代码的意图更清晰了。如果这种效果贯穿于整个项目,那么将显著提升代码库的长期健康。
说到清晰,如果你嫌大括号麻烦,对于简单的函数还有一种更酷的语法。这个迭代语句中的匿名函数(i) {fib_printer(i);}可以写成(i) => fib_printer(i)。这样,我们的代码就变成了下面这样:
var list = [1, 5, 8, 10];
list.forEach((i) => fib_printer(i));
fib_printer(i) {
print(“Fib($i): ${fib(i)}”);
}
fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
参数(i)在匿名函数的定义和fib_printer(i)调用中重复出现了。在JavaScript中,没有更清晰的做法了。然而在Dart中,函数(i) => fib_printer(i) 可以进一步被简化为简单的fib_printer。
var list = [1, 5, 8, 10];
list.forEach(fib_printer);
fib_printer(i) {
print(“Fib($i): ${fib(i)}”);
}
fib(i) {
if (i < 2) return i;
return fib(i-2) + fib(i-1);
}
在这段Dart代码中,这么用确实很简短。
一阶函数
像前面的forEach()方法那样,这种把匿名函数传递到迭代方法中的行为,已经展示出了函数作为头等公民的良好支持,也就是说,可以把函数像变量一样进行赋值和传递。
在写作本书时,Dart语言还缺少一些功能(例如反射)来支持复杂的函数式概念,如curry化或组合(combinator)。不过,在Dart中已经可以执行偏函数应用(partial function application)了。
偏函数应用的典型示例是把一个对3个数字求和的函数转化为固定了其中的一个数字的函数。
add(x, y, z) {
return x + y + z;
}
makeAdder2(fn, arg1) {
return (y, z) {
return fn(arg1, y, z);
};
}
var add10 = makeAdder2(add, 10);
偏函数应用这个名字来自于返回一个已经应用了一个参数的函数。在这里,makeAdder2这个函数返回另外一个接收两个参数的函数。调用这个新函数的结果与调用第一个参数固定为arg1的原函数的结果一样。
在这里,add10()函数接收两个数字参数,对它们求和,再加上10。
add10(1,1); // => 12
可选参数
在JavaScript应用中更繁琐的事情之一是提取可选参数。在Dart语言中使用内建的语法来封装这个概念,解决了这一问题。
像下面这样,把可选参数放在方括号中:
f(a, {b1:’who’, b2, b3, b4, b5, b6, b7}) {
// …
}
可以完全不用任何可选参数来调用这个函数:f(‘foo’)。在这种情况下,函数体中的参数a将被赋值为‘foo’。
要指定可选参数,需要在函数调用中给它们加上参数名作为前缀。
f(‘foo’, b6:’bar’, b3:’baz’);
调用前面这个函数的结果是,在f()方法中,变量a赋值为‘foo’,b1是 ’who’,b3是’baz’,b6是‘bar’。所有其他可选参数b2、b4、b5和b7都是null。
这里要特别注意的是,我们可以在函数参数列表中指定可选参数的默认值。在这个例子中,变量b1的默认值是字符串 ’who’ 。
本文节选自《Dart语言程序设计》一书。斯特罗姆著,由人民邮电出版社出版。