Haskell 的无穷数组
May 01, 2021 · 8 min read
Haskell是一种标准化的,通用的纯函数式编程语言,有惰性求值和强静态类型。因为有惰性求值,所以在 Haskell 中你可以创建一个无限数组。例如
- [0..], 创建一个 0, 1, 2… 不断递增的数组
- repeat 0, 创建一个全是 0 长度为正无穷的数组
- let a = 1:2:a
- 创建一个数组 a,第一个元素是 1,第二个元素是2,剩下来的是 a (没错,可以递归定义)
- 得到一个 [1, 2, 1, 2, 1, 2…] 无限重复的数组
用无穷数组解序列问题
无穷数组对序列的描述能力很强。定义符号 ”++” 代表链接两个序列,那么
等差数列可以这样描述
斐波那契数列的一般描述方式是
但通过递归的无穷数组的方式我们还可以这样表达
- 斐波那契数列 =
对于下面这道题也可以用递归的方法描述
先卖个关子,看看普通的解决思路是什么样的。
一般解法 (TypeScript)
通过链表实现。
Haskell 解法
看起来简单很多有没有!
理解 Haskell 解法
为了方便理解,我们可以泛化序列 u 的定义到 f(n)
在这个定义下,我们需要的序列 u 就等于排序后的 f(1)。
可以注意到 f(n) 符合这样的性质:
Haskell 的解法可以理解为通过无穷数组去描述了这个递归的性质。
因为 Haskell 的语法比较与众不同,为了方便理解,下面是将它朴素地翻译成 JS 之后的样子
next 对应上文提到的 。next 中的递归是没有边界条件的,在没有惰性求值特性的 JS 当中就会死循环。而在 Haskell 中,一个数组被定义后其中的值是没有被求出来的,只有当其中的值被最外层的 IO 获取的时候才会去计算具体的值。
而在 JS 中我们是否有语言特性可以实现惰性求值呢?也有!那就是 Generator 。所以我们可以通过 Generator 构造一个类似的解
在 JS 中通过 Generator 模仿无穷数组
通过无穷数组我们就可以通过 f(n) 的性质 实现:
性能对比
通过 Generator 的方式在性能上一定会有所损耗,以下是 index = 1e6 时的耗时对比。
性能还是比 Haskell 好的!
在 JavaScript 中实现一个惰性数组
Specification
刚刚的例子中我们仅通过 Generator 模仿了 Haskell 解法,那有没有可能在 JS 当中封装一个惰性数组呢?它需要符合以下规格
- 值的计算是 Lazy 的
- 已经被计算的值会被缓存
- 长度可以是无穷大的
- 数组内容不能被改变
- 可获取长度
- 可以连接几个惰性数组,产生一个新的惰性数组
- 可被遍历
- 可获取指定下标的元素
- 定义可以是递归的
从而我们可以这样使用它
v0. 递归 Generator
朴素的 LazyArray 实现(容易爆栈) LazyArray@v0
直接通过递归地调用 self generator 函数就可以完成~
但这个实现方式有一个问题:太容易爆栈了,例如下面的代码就会直接导致溢出。
因为在递归的方式中调用栈是这样的
而栈的深度有限,根据宿主环境不同在几千到几万不等。
v1. 防爆栈优化(存在 Bug)
那有什么方法可以解决这样的爆栈问题呢 —— 将递归转为迭代。
但 LazyArray 的定义是要支持递归的。有什么方法可以让一个递归的定义通过迭代的方式来表达呢?我们可以替换递归过程中使用的函数~
此时我们的调用过程如下图所示,调用栈始终只有一层。
这下怎么都不会溢出了~
emmmmmm,但是这个版本是有 Bug 的。因为这个优化假设了每个 self 函数中 yield 的值都是会被 yield 到最外层的。例如在进行了这一项优化后,下面的例子中的 map 就起不了作用了
因为我们将其中的 self(n) 的 Generator 屏蔽了,导致 map 拿到了空的序列。
所以在进行这项优化时我们还需要判断下一个 self 函数返回的 Generator 是不是直接被上一层的 self 函数的 Generator 消费的。只有这样我们才能进行优化。
v2. 符合条件才进行转递归为迭代
使用它重新解一遍 Twice Linear 也是可以的 (😅 写起来的复杂度没啥变化)
import { LazyArray } from "https://raw.githubusercontent.com/zxch3n/lab/master/lazy-array/lazy-array.ts";
function* merge(a: Iterator<number>, b: Iterator<number>) {
let va = a.next().value!;
let vb = b.next().value!;
while (true) {
if (va < vb) {
yield va;
va = a.next().value;
} else if (va > vb) {
yield vb;
vb = b.next().value;
} else {
yield vb;
va = a.next().value;
vb = b.next().value;
}
}
}
const f = LazyArray.createFactory<number, number>((n: number, self) => {
return LazyArray.concat([n], merge(self(2 * n + 1), self(3 * n + 1)));
});
const ans = f(1);
export function solve(i: number) {
return ans.get(i);
}
LazyArray 的实现:zxch3n/lab
TwiceLinear 相关代码:zxch3n/blog