
JS 不像 Haskell,其自身从语言设计层面不支持惰性求值,但是可以通过语法去 模拟实现 这一特性;
想一想,我们可以用什么来 JS 语法来模拟这一“延迟计算”的特性?
没思路的话,看前篇这一句:
在《Haskell趣学指南》中,thunk 被翻译成 保证;
在《Haskell 函数式编程入门》,thunk 被解释为:
thunk 意为形实替换程序(有时候也称为延迟计算,suspended computation)。它指的是在计算的过程中,一些函数的参数或者一些结果通过一段程序来代表,这被称为 thunk。可以简单地把 thunk 看做是一个未求得完全结果的表达式与求得该表达式结果所需要的环境变量组成的函数,这个表达式与环境变量形成了一个无参数的闭包(parameterless closure),所以 thunk 中有求得这个表达式所需要的所有信息,只是在不需要的时候不求而已。
那意思是用 Promise 模拟吗?
事实上,不行!
Promise
一旦执行,它就开始执行了,你只知道是在
Pending
,但不知道是刚开始执行,或者是快执行完了,还是其它哪个执行阶段;获取
Promise
的时候,内部的异步任务就已经启动了,执行无法中途取消(这也是 Promise 的弊端之一:);
代码????
Promise 是立即求值,不是惰性求值!
那手上还有什么牌?
“延迟执行”不就是“暂停以后再执行”嘛?
thunk
更像是
Generator
!!????
赋值的时候,我不进行计算,把你包装成一个
<suspended>
暂停等待,等你调用
next()
的时候,我再计算;
代码????
这不就是最简单版本的 JS 惰性求值 Thunk 的实现吗?
Haskell 中的无限列表不就是 MDN 中 Generator 所实现的 无限迭代器 吗?
function* idMaker(){
let index = 0;
while(true)
yield index++;
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
// 0
console.log(gen.next().value);
// 1
console.log(gen.next().value);
// 2
// ...
复制代码
实际上 Lazy.js 也正是借助 Generator 实现“惰性”的!
以实现
take
方法为例????:
在 Haskell 中,
take
函数可以从头连续地取得一个列表的几个元素;
Prelude> take 3 [1,2,3,4,5]
[1,2,3]
复制代码
JS 模拟实现
take
:
function* take(n,items){
let i = 0;
if (n < 1) return;
for (let item of items) {
yield item;
i++;
if (i >= n) {
return;
}
}
}
let thunk=take(3,[1,2,3,4,5])
console.log(thunk.next()) // {value: 1, done: false}
console.log(thunk.next()) // {value: 2, done: false}
console.log(thunk.next()) // {value: 3, done: false}
console.log(thunk.next()) // {value: undefined, done: true}
复制代码
使用 lazy.js 是类似这样调用的:
Lazy(stream)
.take(5) // 仅仅阅读数据中的前五块内容
.each(processData);
小结
专栏介绍引用的是这句话:
如果要整体了解一个人的核心 JavaScript 技能,我最感兴趣的是他们会如何使用闭包以及如何充分利用异步。—— Jake Archibald
再回看 wiki 上关于闭包的这句解释:
- 闭包的用途:
(暂且不论用于生成这个闭包对象本身的开销,比如 C++ 中按值捕获意味着执行复制构造函数),即“惰性求值”,所以它可以被用来定义控制结构。例如:在Smalltalk语言中,所有的控制结构,包括分支条件(if/then/else)和循环(while和for),都是通过闭包实现的。用户也可以使用闭包定义自己的控制结构。因为闭包只有在被调用时才执行操作
现在看来,惰性求值似乎能连接“如何使用闭包”和“如何充分利用异步”!!
“惰性”的思想深入函数式编程,还有最重要的 Monad,把具有“副作用”的部分延后处理,也与“惰性”呼应,后面有机会再讨论~