第三周主要介绍了 头等函数 (First-class function) 与 函数闭包 (Function Closure)的概念。
头等函数传递出一个重要的概念 :Functions are values,即函数可以作为参数传递,计算以及存储。而 函数闭包 则是指函数可以使用定义在其之外的变量,这使得头等函数的功能更为强大。
高阶函数 (High-order function) 指以函数作为参数或以函数作为返回值的函数。
函数式编程 (Functional programming) 的一些特点:
- Mutation avoided.
- Using functions as values.
Anonymous Function
匿名函数 (Anonymous Function) 是无名的函数,可以很方便的作为参数传入其他函数。但由于其无名的特性,其不能实现递归。
(fn x => e)
我们可以这样认为,关键字 fun
实际上是一种语法糖。如下:
fun x = e
val x = fn x => e
两个表达实际是一致的。
在使用匿名函数时,要注意避免 Unnecessary function wrapping
fn x => tl x (* the same as tl *)
High-order function
这里介绍几个比较常用的高阶函数,map filter zip
这些函数可以说是高阶函数中的 hall of fame,在各个语言中都有出现
其中 map 将函数 f 应用到 list 中的所有元素上并产生一个新 list
fun map(f xs) =
case xs of
[] => NONE
| x :: xs => f x :: map(f, xs)
filter 函数将 f 应用到 list 中的所有元素,保留其中 f evaluate 为 true 的元素
fun filter(f xs) =
case xs of
[] => NONE
| x :: xsx =>
if f x = true then x :: filter(f xs) else filter(f xs)
zip函数需要传入两个长度一致的list, 将对应位置的元素提取出来作为一个pair再组成一个新的 pair list
Lexical scope
目前为止,我们定义的高阶函数都是闭合 (close) 的。也就是说,函数的主体部分仅使用函数的参数以及本地定义的变量。
但实际上,函数可以使用的变量不止这些。
在 week 1 中我们强调过这么一个概念:
The body of a function is evaluated in the environment where the function is defined, not the environment where the function is called.
一个声明被调用时的作用域不一定是它的词法作用域。相反的,定义时的作用域才是词法作用域。
我们使用这一个例子来说明:
val x = 1
fun f y = x + y
val x = 2
val y = 3
val z = f (x + y)
在调用 f 函数时,我们 evaluate f 函数的主体部分所参考的是其定义时的环境,在这个环境中 x 的值是 1。
因此最后 z 的值为 6 而不是 5。
可以发现,evaluate argument 是在 current environment 中进行的,而 evaluate function body 是在所谓的 old environment 中进行的。这一特征就被称为词法作用域 (lexical scope)。
静态作用域相比于动态作用域 (Dynamic scope) 有许多显而易见的好处:不会受变量名称改变的影响,有点类似于 immutable 相对于 mutable 的好处。
Closure
我们强调过 Function is value 的概念。而 Function value 实际上包含两个部分:函数的代码 与 定义函数时的环境 (the function code and the environment than was current when we created the function)。这就成为函数闭包或闭包 (Closure)。闭包记录了一切该函数所需的 bindings 信息,因此闭包整体来说是闭合的:它包含一切它所需要的东西,以在传入参数后可以产生相应的结果。
在上面的例子中,语句 fun f y = x + y
创建了一个函数 f 闭包。它包括 代码部分 fn y => x + y
以及其所在的环境,在这个环境中 x 的值为 1。所以,在任何情况下调用 f 函数最终返回的都是 y + 1。使用该函数时的环境同调用它时的环境完全是隔绝的,这也是闭包的另一种理解。
High-order functions, combining functions, callbacks and abstract datatype
词法作用域与闭包的存在使得许多功能得以实现。
其中,高阶函数与函数的复合就是如此。由于闭包保证了一个函数不需依赖环境之外的变量进行运行,任何一个函数都可以作为一个独立的环境以参数形式传递给其他的函数。同理,也可以实现函数的复合:某个函数的返回值可以作为另一个函数的参数。
在 ML 语言中,我们使用连接符 o
来复合函数,顺序同数学中一样,是由右至左的。
val x = f o g o h a
(* x = f(g(h(a))) *)
同样,闭包的特性也支持回调 (callbacks) 与 抽象数据类型(Abstract data types) 的实现。 (*)
回调指的是一种特殊的函数,其作为参数传入到另一个函数中进行调用。
而抽象数据类型实现于程序时,只显现出其接口,并将实现加以隐藏。用户只需关心它的接口,而不是如何实现,未来更可以改变实现的方式。抽象数据类型的接口即为调用 (call),而内部完全独立封装,这正好契合了闭包的特性。闭包则记录该 ADT 所使用的 private field 与 private method。在 OOP 语言中通过定义 class 来实现 ADT,这使我们发现 OOP 与 FP 的一些深层共性。
Currying and Partial application
之前我们提到过,在 ML 语言中,每一个函数都只有一个参数。所谓的 “多参数函数” 可以视为一个包含多个变量的 tuple。
然而除此之外,有另一种方式可以实现所谓的 “多参数” ,那就是 柯里化函数 (Currying)。
柯里化函数的实现同样离不开词法作用域。
柯里化函数具体实现如下,通过将多对一函数拆成连续几个一对一函数从而实现:
fun p(x, y, z) = e
fun curry_p = fn x => fn y => fn z => e
fun curry_p2 x y z = e
第三条语句是第二条语句的语法糖。三个函数的作用完全相同。
柯里化函数还能部分化应用,从而减少不必要的参数。例如:
fun curry_p x y z = ... z
fun partial_curry_p x y = ...
这样消去了 redundant z。
fun curry f x y = f(x, y)
fun uncurry f (x, y) = f x y