基于 Generator 和 Iterator 的惰性列表

栏目: JavaScript · 发布时间: 5年前

内容简介:如果有了解过 Haskell 的朋友,对下面的这些表达一定不陌生:上面的几个表达式产生的都是无限列表。对于习惯了主流编程语音的朋友可能感到困惑,在有限的内存里面如何能表达无限的概念。主要的原因就是 Haskell 是一门默认采用惰性求值策略的语言,没有用到的部分,在内存里面只是一个表达式,并不会真正的去做计算。

文 | 王桥 on 前端

初识 Lazy List 基于 Generator 和 Iterator 的惰性列表

如果有了解过 Haskell 的朋友,对下面的这些表达一定不陌生:

上面的几个表达式产生的都是无限列表。对于习惯了主流编程语音的朋友可能感到困惑,在有限的内存里面如何能表达无限的概念。主要的原因就是 Haskell 是一门默认采用惰性求值策略的语言,没有用到的部分,在内存里面只是一个表达式,并不会真正的去做计算。

如果只看上面的几个表达式,很多朋友可能会说,也没感觉到有什么神奇的地方,似乎并没有什么作用。我们再看看下面的代码。

Haskell 中的 fibonacci 数列:

这里 fibonacci 本身是一个惰性结构,所以在计算的时候,会先算出列表前面的两个1,得到 1:1... 这样的结构,然后怎么表达 fibonacci fib(n)=fib(n-1)+fib(n-2) 特性呢?我们可以注意到, n-1 n-2 刚好在数列中相差一位,所以 n 可以看作是该数列错位的相加的结果。

我们再来看一则 筛法求素数 。不熟悉筛法的可以先点开 wiki 去看一下该算法的思路。下面这段代码是 Haskell 的一个简单实现。

So, Why Lazy? 基于 Generator 和 Iterator 的惰性列表

在某些不定长度的列表操作上,惰性列表会让代码和结构更灵活。用上面的 primes 列表举个例子好了,在传统的 C 语言或者 Java 的实现里面,我们一般要先声明一个最大长度或者一个最大的取值范围,比如 10000 以内的素数。如果后面的计算要用到超过这个范围,我们就不得不重新调用生成函数,重新生成一份更长的列表。这里面的问题是:一、要主动去调用这个工厂函数,二、如果要复用已经计算出来的数据,手动去维护一个cache列表,势必增加代码的复杂度。另外一个可能的情况是,我们预先生成了一份很长的列表,后面的计算中只用到了列表头部的一丢丢数据,这也是极大的浪费。

惰性列表的使用增加了我们编程的表达能力,让我们可以更关注数据结构本身的特性,而不是浪费时间在如何去管理堆栈上面。因为,惰性求值特性保证我们在需要一个值的时候才会去计算,所以可以自动地最小化我们的计算量,节约资源。

比如我们可以通过 lazy byteString 去读、写文件,它本身不会把整个文件加载到我们的内存里面,而是按需的读取。有的时候我们读一个大文件,可能只筛选出需要的前几十条数据,却确不得不把几百 M 甚至上 G 的大文件整个的放到内存里面。

这里也找到一篇14年的文章 How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation ,感兴趣的可以点开看看。

在 JavaScript 中实现 Lazy List 基于 Generator 和 Iterator 的惰性列表

在 JavaScript 有没有惰性结构呢?先看下面这个例子。

fetch 方法本身是立即执行的,如果满足条件,这里的 fetch 方法会执行两次。这并不是我们期待的行为,这里需要让这个 fetch 的动作在我们需要的时候才去执行,而不是声明的时候就开始执行的话,通常的做法是把它改成下面的样子。

由此启发,我们大致可以实现如下的结构。

List<T> 本质上是一个单链表,构造函数里面传入的 tail 是一个工厂函数,用来构建新的 List 节点。只有在我们访问到一个节点的时候,才对它的 head 求值,访问它的下一个节点的时候对 tail 求值,不然 head 和 tail 都只是待求值的表达式。

这种方式看起来似乎已经解决了我的问题,但是这种结构在和普通的 Array 做互相转换的时候,存在大量不必要的额外开销。

那 JavaScript 中有没有更天然的结构,可以让我们免于去构造这样一个复杂的对象,简化代码的同时,让我们的代码更具有普适性呢?

初识 Iterable

ES6 的新特性给了我想要的答案, Iteration Protocols 。如果嫌MDN的描述太长,可以直接看下面等价的类型声明。

所有实现一个Iterable接口的对象都可以通过诸如 for...of... ...itor 以及 Array.from 来访问,当next方法的返回值中done为true时,迭代结束。而且只有我们访问next方法时,才会进入下一步迭代,是理想的Lazy结构。

这时候我们看一下我们的 fibonacci 该怎么写?

到这里,我们已经可以表达一个惰性的无限数列了。但是上面的代码毕竟过于繁琐,好在 ES6 同时给我们提供了 Generator, 可以让我们很方便地书写 IterableItorator,从某种意义上来讲,Generator 可以说是上面代码的语法糖。

使用Generator,上面的代码可以简化成下面的样子。

这里不再去花段落介绍 Generator 的语法,不了解的同学可以先去阅读下这篇文章 A Simple Guide to Understanding Javascript (ES6) Generators

定义 Infinite List

接着上面的代码往下写,下面的代码分别实现了文章开头的 repeat, cycle, iterate, range 等方法。

可以看到,代码是非常直观且易于理解的。

定义 Operator

有了列表之后,我们需要在列表之上进行操作,下面的代码分别实现了 map/filter/take/takeWhile 方法。

上面的代码都是比较简单的。比较难一点的是去实现 zip 方法,即怎么把两个列表合并成一个?

难点在于接收一个 Iterable 的对象的话,本身并不一定要实现 next 方法的,比如 Array、String 等,同时Iterable对象也并不是都可以通过 index 来访问的。此外,如果想先通过Array.from变成数组,然后在数组上进行操作,我们会遇到一个情况是我们传入的 Iterable 对象是无限的,如上文的 fibonacci 一样,这种情况下是不能使用 Array.from 的。

这时候我的一个思路是需要想办法把一个 Iterable 的对象提升成为 IterableItorator 对象,然后通过 next 方法,逐一遍历。

How ?幸好 Generator 给我们提供了一个 yield* 操作符,可以让我们方便的定义出一个   lift 方法。

有了这个 lift 方法之后,就可以很方便的书写 zip 方法和 zipWith 方法了。

更多的方法可以去底部的点开我的 repo,这里就不一一列举了。

结语 基于 Generator 和 Iterator 的惰性列表

Generator 和 Iterator 是 ES6 带给我们的非常强大的语言层面的能力,它本身的求值可以看作是惰性的。

差不多在13年左右,TJ 的 co 刚出来的时候,其代码的短小精悍可以说是相当惊艳的。然而在我们的使用中,一来受限于浏览器兼容性,二来受限于我们的使用场景,个人认为我们对其特性开发得还远远不够。结合 IO、network,Generator 和 Iterator 还能为我们做更多的事情。

另外,需要特别说明的是,虽然这篇文章通篇是在讲惰性列表,但是惰性列表并不是银弹,相反的,惰性结构的滥用会在程序的执行过程中缓存大量的thunk,增大在内存上的开销。

最后,利益相关 - 有赞招前端,简历请投 wangqiao@youzan.com

  • GitHub lazyList。

  • Slide Lazy List With Generator and Iterator

基于 Generator 和 Iterator 的惰性列表


以上所述就是小编给大家介绍的《基于 Generator 和 Iterator 的惰性列表》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法分析

数据结构与算法分析

韦斯 (Mark Allen Weiss) / 机械工业出版社 / 2009-1-1 / 55.00元

本书是国外数据结构与算法分析方面的经典教材,使用卓越的Java编程语言作为实现工具讨论了数据结构(组织大量数据的方法)和算法分析(对算法运行时间的估计)。 随着计算机速度的不断增加和功能的日益强大,人们对有效编程和算法分析的要求也不断增长。本书把算法分析与最有效率的Java程序的开发有机地结合起来,深入分析每种算法,内容全面、缜密严格,并细致讲解精心构造程序的方法。一起来看看 《数据结构与算法分析》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具