以我的拙见,我认为您从错误的角度看待这个问题。如果您要手动创建 thunk,那么您需要考虑重构您的代码。在大多数情况下,thunk 应该是:
- 从惰性函数返回。
- 或通过组合函数创建。
从惰性函数返回 Thunk
当我第一次开始在 JavaScript 中练习函数式编程时,我被Y 组合器迷住了。根据我在网上阅读的内容,Y 组合子是一个值得崇拜的神圣实体。它以某种方式允许不知道自己名字的函数调用自己。因此,它是递归的数学表现——函数式编程最重要的支柱之一。
然而,理解 Y 组合器并非易事。Mike Vanier写道,Y 组合器的知识是“功能性识字”和不识字的人之间的跳水线。老实说,Y 组合器本身很容易理解。但是,大多数在线文章都将其向后解释,使其难以理解。例如 Wikipedia 将 Y 组合器定义为:
Y = λf.(λx.f (x x)) (λx.f (x x))
在 JavaScript 中,这将转换为:
function Y(f) {
return (function (x) {
return f(x(x));
}(function (x) {
return f(x(x));
}));
}
Y 组合器的这个定义是不直观的,并且它并不清楚 Y 组合器是如何体现递归的。更不用说它根本不能在像 JavaScript 这样的急切语言中使用,因为表达式x(x)
会立即计算,从而导致无限循环,最终导致堆栈溢出。因此,在像 JavaScript 这样的热切语言中,我们使用 Z 组合器来代替:
Z = λf.(λx.f (λv.((x x) v))) (λx.f (λv.((x x) v)))
JavaScript 中生成的代码更加混乱和不直观:
function Z(f) {
return (function (x) {
return f(function (v) {
return x(x)(v);
});
}(function (x) {
return f(function (v) {
return x(x)(v);
});
}));
}
我们可以看到,Y 组合器和 Z 组合器之间的唯一区别是惰性表达式x(x)
被急切表达式取代function (v) { return x(x)(v); }
。它被包裹在一个thunk中。然而,在 JavaScript 中,将 thunk 编写如下更有意义:
function () {
return x(x).apply(this, arguments);
}
当然,这里我们假设x(x)
计算为一个函数。在 Y 组合器的情况下,这确实是正确的。但是,如果 thunk 不计算为函数,那么我们只需返回表达式。
作为一名程序员,对我来说最顿悟的时刻之一就是 Y 组合器本身就是递归的。例如在 Haskell 中,你定义 Y 组合器如下:
y f = f (y f)
因为 Haskell 是一种惰性语言,只有在需要时才对y f
inf (y f)
进行评估,因此您不会陷入无限循环。在内部,Haskell 为每个表达式创建一个 thunk。然而,在 JavaScript 中,您需要显式创建一个 thunk:
function y(f) {
return function () {
return f(y(f)).apply(this, arguments);
};
}
当然,递归地定义 Y 组合器是作弊:您只是在 Y 组合器内部显式递归。从数学上讲,Y 组合器本身应该被定义为非递归来描述递归的结构。尽管如此,无论如何我们都喜欢它。重要的是 JavaScript 中的 Y 组合器现在返回一个 thunk(即我们使用惰性语义定义它)。
为了巩固我们的理解,让我们在 JavaScript 中创建另一个惰性函数。让我们repeat
在 JavaScript 中实现 Haskell 中的函数。在 Haskell 中,repeat
函数定义如下:
repeat :: a -> [a]
repeat x = x : repeat x
如您所见repeat
,没有边缘情况,它递归地调用自己。如果 Haskell 不是那么懒惰,它将永远递归。如果 JavaScript 是惰性的,那么我们可以实现repeat
如下:
function repeat(x) {
return [x, repeat(x)];
}
不幸的是,如果执行上述代码将永远递归,直到导致堆栈溢出。为了解决这个问题,我们返回一个 thunk 代替:
function repeat(x) {
return function () {
return [x, repeat(x)];
};
}
当然,由于 thunk 不会评估为函数,我们需要另一种方法来同等对待 thunk 和正常值。因此,我们创建一个函数来评估一个 thunk,如下所示:
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
该evaluate
函数现在可用于实现可以将惰性或严格数据结构作为参数的函数。例如,我们可以take
使用 Haskell 实现该功能evaluate
。在 Haskelltake
中定义如下:
take :: Int -> [a] -> [a]
take 0 _ = []
take _ [] = []
take n (x:xs) = x : take (n - 1) xs
在 JavaScript 中,我们将take
使用evaluate
如下实现:
function take(n, list) {
if (n) {
var xxs = evaluate(list);
return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
} else return [];
}
现在您可以将repeat
andtake
一起使用,如下所示:
take(3, repeat('x'));
亲自查看演示:
alert(JSON.stringify(take(3, repeat('x'))));
function take(n, list) {
if (n) {
var xxs = evaluate(list);
return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
} else return [];
}
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
function repeat(x) {
return function () {
return [x, repeat(x)];
};
}
工作中的懒惰评价。
在我看来,大多数 thunk 应该是惰性函数返回的那些。您永远不必手动创建 thunk。但是,每次创建惰性函数时,您仍然需要在其中手动创建一个 thunk。这个问题可以通过提升惰性函数来解决,如下所示:
function lazy(f) {
return function () {
var g = f, self = this, args = arguments;
return function () {
var data = g.apply(self, args);
return typeof data === "function" ?
data.apply(this, arguments) : data;
};
};
}
使用该lazy
函数,您现在可以定义 Y 组合器,repeat
如下所示:
var y = lazy(function (f) {
return f(y(f));
});
var repeat = lazy(function (x) {
return [x, repeat(x)];
});
这使得 JavaScript 中的函数式编程几乎与 Haskell 或 OCaml 中的函数式编程一样有趣。查看更新的演示:
var repeat = lazy(function (x) {
return [x, repeat(x)];
});
alert(JSON.stringify(take(3, repeat('x'))));
function take(n, list) {
if (n) {
var xxs = evaluate(list);
return xxs.length ? [xxs[0], take(n - 1, xxs[1])] : [];
} else return [];
}
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
function lazy(f) {
return function () {
var g = f, self = this, args = arguments;
return function () {
var data = g.apply(self, args);
return typeof data === "function" ?
data.apply(this, arguments) : data;
};
};
}
通过组合函数创建 Thunks
有时您需要将表达式传递给延迟评估的函数。在这种情况下,您需要创建自定义 thunk。因此我们无法使用该lazy
功能。在这种情况下,您可以使用函数组合作为手动创建 thunk 的可行替代方案。函数组合在 Haskell 中定义如下:
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)
在 JavaScript 中,这转换为:
function compose(f, g) {
return function (x) {
return f(g(x));
};
}
但是,将其写为:
function compose(f, g) {
return function () {
return f(g.apply(this, arguments));
};
}
数学中的函数组合从右到左读取。然而,JavaScript 中的评估总是从左到右。例如,在表达式slow_foo().toUpperCase()
中,首先执行函数slow_foo
,然后根据toUpperCase
返回值调用方法。因此,我们希望以相反的顺序组合函数并将它们链接如下:
Function.prototype.pipe = function (f) {
var g = this;
return function () {
return f(g.apply(this, arguments));
};
};
使用该pipe
方法,我们现在可以按如下方式组合函数:
var toUpperCase = "".toUpperCase;
slow_foo.pipe(toUpperCase);
上面的代码将等效于以下 thunk:
function () {
return toUpperCase(slow_foo.apply(this, arguments));
}
然而有一个问题。该toUpperCase
函数实际上是一个方法。因此返回的值slow_foo
应该设置 的this
指针toUpperCase
。简而言之,我们希望将slow_foo
into的输出通过管道传输toUpperCase
如下:
function () {
return slow_foo.apply(this, arguments).toUpperCase();
}
解决方案其实很简单,我们根本不需要修改我们的pipe
方法:
var bind = Function.bind;
var call = Function.call;
var bindable = bind.bind(bind); // bindable(f) === f.bind
var callable = bindable(call); // callable(f) === f.call
使用该callable
方法,我们现在可以重构我们的代码,如下所示:
var toUpperCase = "".toUpperCase;
slow_foo.pipe(callable(toUpperCase));
因为callable(toUpperCase)
相当于toUpperCase.call
我们的 thunk 现在是:
function () {
return toUpperCase.call(slow_foo.apply(this, arguments));
}
这正是我们想要的。因此我们的最终代码如下:
var bind = Function.bind;
var call = Function.call;
var bindable = bind.bind(bind); // bindable(f) === f.bind
var callable = bindable(call); // callable(f) === f.call
var someobj = {x: "Quick."};
slow_foo.times_called = 0;
Function.prototype.pipe = function (f) {
var g = this;
return function () {
return f(g.apply(this, arguments));
};
};
function lazyget(obj, key, lazydflt) {
return obj.hasOwnProperty(key) ? obj[key] : evaluate(lazydflt);
}
function slow_foo() {
slow_foo.times_called++;
return "Sorry for keeping you waiting.";
}
function evaluate(thunk) {
return typeof thunk === "function" ? thunk() : thunk;
}
然后我们定义测试用例:
console.log(slow_foo.times_called);
console.log(lazyget(someobj, "x", slow_foo()));
console.log(slow_foo.times_called);
console.log(lazyget(someobj, "x", slow_foo.pipe(callable("".toUpperCase))));
console.log(slow_foo.times_called);
console.log(lazyget(someobj, "y", slow_foo.pipe(callable("".toUpperCase))));
console.log(slow_foo.times_called);
console.log(lazyget(someobj, "y", "slow_foo().toUpperCase()"));
console.log(slow_foo.times_called);
结果和预期的一样:
0
Quick.
1
Quick.
1
SORRY FOR KEEPING YOU WAITING.
2
slow_foo().toUpperCase()
2
因此,正如您所见,在大多数情况下,您永远不需要手动创建 thunk。使用函数提升函数lazy
以使其返回 thunk 或组合函数以创建新的 thunk。