Thunk函数的含义和用法

弄清楚 Thunk 函数的前提下先看两个定义,什么是求值策略?

求值策略

求值策略即编程语言的函数在进行参数传递时,函数的参数到底应该什么时候进行计算求值比较好;这里也是一个争论点,观点一是“传值调用(call by value)”,观点二是“传名调用(call by name)”;那么这两种观点有什么区别呢?

  1. 传值调用

例如:

1
2
3
4
5
6
var x = 1;
funciton f(m) {
return m * 2;
}

f(x + 5);

在这个函数上如果用传值调用(call by value)的话,即在进入函数体之前就计算 x + 5 的值(等于 6),再将已经求出的这个值传入函数 f;c 语言就是这种策略。

1
2
3
f(x + 5)
// 传值调用,等同于
f(6)
  1. 传名调用

还是前面的例子,如果用传名调用(call by name)的话,即直接将表达式 x + 5 传入函数体,只在函数体内部用到它的时候进行求值; Hskell 语言就是采用的这种策略。

1
2
3
f(x + 5)
// 传名调用,等同于
(x + 5) * 2

通过上面两个例子的说明,传值调用和传名调用哪一种方法比较好,回答是各有利弊。这里的传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

1
2
3
4
5
function f(a, b) {
return b;
}

f(3 * x * x - 2 * x - 1, x);

这里的示例中就是在函数 f 的参数中传入了两个参数并进行了大量计算,而函数体内并没有用到第一个进行大量计算的参数值,实际上对这个参数的求值就是没有必要的;因此,有一些人更倾向于“传名调用”,即在执行的时候求值。

Thunk 函数的含义

在编译器中的“传名调用”实现时,往往是将参数放到一个临时函数中,再将这个临时函数传入函数体。而这个临时函数就叫做 Thunk 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function f(m) {
return m * 2;
}
f(x + 5);

// 等同于

var thunk = function () {
return x + 5;
}
function f(thunk) {
return thunk() * 2;
}

在这里,函数 f 中的参数 x + 5 被一个函数替换了,凡是用到原参数的地方,对 Thunk 函数求值即可。
**这就是 Thunk 函数的定义,它是“传名调用”的一种实现策略,用来替换某个表达式。

JavaScript 语言的 Thunk 函数

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中, Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
// 正常版本的 readFile(多参数)
fs.readFile(filename, callback);

// Thunk 版本的 readFile(单参数)
var readFileThunk = Thunk(filename);
readFileThunk(callback);

var Thunk = function(filename) {
return function (callback) {
return fs.readFile(filename, callback);
}
}

这里把 fs 模块的 readFile 方法从多个参数转换成单个参数的函数后,这个单参数的版本就是 Thunk 函数。

最后,只要参数有回调函数,就能写成 Thunk 函数的形式,这里给出一个 Thunk 函数转换器做记录(来自阮老师):

1
2
3
4
5
6
7
8
9
10
11
12
13
var Thunk = function (fn) {
return function() {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
args.push(callback);
return fn.apply(this, args);
}
}
}

//将上面的 readFile 用转换器做一个处理
var readFileThunk = Thunk(fs.readFile);
readFileThunk(filename)(callback);

总结
Thunk 函数其实就是“传名调用”的一种实现策略,将参数放到一个临时函数并将临时函数传入函数体进行执行。