首先,让我为那些不了解的人定义short-cut fusion是什么。在JavaScript中考虑以下数组转换:
var a = [1,2,3,4,5].map(square).map(increment);
console.log(a);
function square(x) {
return x * x;
}
function increment(x) {
return x + 1;
}

这里我们有一个数组[1,2,3,4,5]
,其元素首先被平方,[1,4,9,16,25]
,然后递增[2,5,10,17,26]
。因此,虽然我们不需要中间数组[1,4,9,16,25]
,但我们仍然会创建它。
捷径融合是一种优化技术,它可以通过将一些函数调用合并为一个来消除中间数据结构。例如,可以将快捷融合应用于上述代码以产生:
var a = [1,2,3,4,5].map(compose(square, increment));
console.log(a);
function square(x) {
return x * x;
}
function increment(x) {
return x + 1;
}
function compose(g, f) {
return function (x) {
return f(g(x));
};
}

正如您所看到的,通过撰写map
和map
函数,两个单独的square
调用已融合到一个increment
调用中。因此,不会创建中间数组。
现在,我了解像Immutable.js和Lazy.js这样的库在JavaScript中模拟延迟评估。延迟评估意味着只在需要时计算结果。
例如,考虑上面的代码。虽然我们square
和increment
数组的每个元素,但我们可能不需要所有结果。
假设我们只想要前3个结果。使用Immutable.js或Lazy.js,我们可以获得前3个结果[2,5,10]
,而无需计算最后2个结果[17,26]
,因为它们不是必需的。
然而,懒惰的评估只会延迟结果的计算直到需要。它不会通过融合函数来删除中间数据结构。
为了明确这一点,请考虑以下代码来模拟延迟评估:
var List = defclass({
constructor: function (head, tail) {
if (typeof head !== "function" || head.length > 0)
Object.defineProperty(this, "head", { value: head });
else Object.defineProperty(this, "head", { get: head });
if (typeof tail !== "function" || tail.length > 0)
Object.defineProperty(this, "tail", { value: tail });
else Object.defineProperty(this, "tail", { get: tail });
},
map: function (f) {
var l = this;
if (l === nil) return nil;
return cons(function () {
return f(l.head);
}, function () {
return l.tail.map(f);
});
},
take: function (n) {
var l = this;
if (l === nil || n === 0) return nil;
return cons(function () {
return l.head;
}, function () {
return l.tail.take(n - 1);
});
},
mapSeq: function (f) {
var l = this;
if (l === nil) return nil;
return cons(f(l.head), l.tail.mapSeq(f));
}
});
var nil = Object.create(List.prototype);
list([1,2,3,4,5])
.map(trace(square))
.map(trace(increment))
.take(3)
.mapSeq(log);
function cons(head, tail) {
return new List(head, tail);
}
function list(a) {
return toList(a, a.length, 0);
}
function toList(a, length, i) {
if (i >= length) return nil;
return cons(a[i], function () {
return toList(a, length, i + 1);
});
}
function square(x) {
return x * x;
}
function increment(x) {
return x + 1;
}
function log(a) {
console.log(a);
}
function trace(f) {
return function () {
var result = f.apply(this, arguments);
console.log(f.name, JSON.stringify([...arguments]), result);
return result;
};
}
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}

正如您所看到的,函数调用是交错的,只处理数组的前三个元素,证明结果确实是懒惰计算的:
square [1] 1
increment [1] 2
2
square [2] 4
increment [4] 5
5
square [3] 9
increment [9] 10
10
如果未使用延迟评估,则结果为:
square [1] 1
square [2] 4
square [3] 9
square [4] 16
square [5] 25
increment [1] 2
increment [4] 5
increment [9] 10
increment [16] 17
increment [25] 26
2
5
10
但是,如果您看到源代码,则每个函数list
,map
,take
和mapSeq
都会返回一个中间List
数据结构。没有进行捷径融合。
这让我想到了一个主要问题:像Immutable.js和Lazy.js这样的库是否会执行短切融合?
我问的原因是因为根据文件,他们“显然”做了。但是,我持怀疑态度。我怀疑他们是否真的进行了短切融合。
例如,这取自Immutable.js的README.md文件:
Immutable
还提供了一个惰性Seq
,允许有效链接收集方法,如map
和filter
,而无需创建中间表示。使用Seq
和Range
创建一些Repeat
。
因此,Immutable.js的开发人员声称他们的Seq
数据结构允许有效链接收集方法,如map
和filter
,而无需创建中间表示(即他们进行短切融合。)
但是,我不会在他们code的任何地方看到他们这样做。也许我找不到它,因为他们使用的是ES6,我的眼睛并不熟悉ES6语法。
此外,他们在Lazy Seq的文档中提到:
Seq
描述了一个惰性操作,允许它们有效地链接使用所有Iterable方法(例如map
和filter
)。Seq是不可变的 - 创建Seq后,无法更改,附加,重新排列或以其他方式修改。相反,调用Seq的任何变异方法都将返回一个新的Seq。
Seq是懒惰的 - Seq尽可能少地响应任何方法调用。
因此确定Seq
确实是懒惰的。但是,没有示例表明中间表示确实没有创建(他们声称正在做)。
继续使用Lazy.js我们也有同样的情况。值得庆幸的是,Daniel Tao写了一篇关于Lazy.js如何工作的blog post,其中他提到Lazy.js的核心是函数组合。他给出了以下例子:
Lazy.range(1, 1000)
.map(square)
.filter(multipleOf3)
.take(10)
.each(log);
function square(x) {
return x * x;
}
function multipleOf3(x) {
return x % 3 === 0;
}
function log(a) {
console.log(a);
}

<script src="https://rawgit.com/dtao/lazy.js/master/lazy.min.js"></script>
&#13;
此处map
,filter
和take
函数会生成中间MappedSequence
,FilteredSequence
和TakeSequence
个对象。这些Sequence
对象本质上是迭代器,不需要中间数组。
然而,据我所知,仍然没有发生捷径融合。中间数组结构简单地用未融合的中间Sequence
结构替换。
我可能错了,但我相信像Lazy(array).map(f).map(g)
这样的表达式产生两个独立的MappedSequence
个对象,其中第一个MappedSequence
对象将其值提供给第二个,而不是第二个通过完成两者的工作(通过功能组合)替换第一个。
TLDR: Immutable.js和Lazy.js确实执行短切融合吗?据我所知,他们通过序列对象(即迭代器)模拟延迟评估来摆脱中间数组。但是,我相信这些迭代器是链接的:一个迭代器懒洋洋地将它的值提供给下一个迭代器。它们不会合并为单个迭代器。因此,他们不“消除中间表征”。它们只将数组转换为常量空间序列对象。
答案 0 :(得分:35)
我是Immutable.js的作者(也是Lazy.js的粉丝)。
Lazy.js和Immutable.js的Seq是否使用捷径融合?不,不完全是。但它们确实删除了操作结果的中间表示。
捷径融合是一种代码编译/转换技术。你的榜样很好:
var a = [1,2,3,4,5].map(square).map(increment);
Transpiled:
var a = [1,2,3,4,5].map(compose(square, increment));
Lazy.js和Immutable.js不是转换器,也不会重写代码。它们是运行时库。因此,它们使用可迭代组合(运行时技术)而不是捷径融合(编译器技术)。
您在TLDR中回答这个问题:
据我所知,他们通过模仿懒惰来摆脱中间数组 通过序列对象(即迭代器)进行评估。但是,我相信 这些迭代器是链接的:一个迭代器提供它的值 懒洋洋地说到下一个。它们不会合并为单个迭代器。于是 他们没有&#34;消除中间表示&#34;。他们只 将数组转换为常量空间序列对象。
这是完全正确的。
让我们解压缩:
Arrays在链接时存储中间结果:
var a = [1,2,3,4,5];
var b = a.map(square); // b: [1,4,6,8,10] created in O(n)
var c = b.map(increment); // c: [2,5,7,9,11] created in O(n)
捷径融合转化创造了中间函数:
var a = [1,2,3,4,5];
var f = compose(square, increment); // f: Function created in O(1)
var c = a.map(f); // c: [2,5,7,9,11] created in O(n)
可迭代合成创建中间可迭代:
var a = [1,2,3,4,5];
var i = lazyMap(a, square); // i: Iterable created in O(1)
var j = lazyMap(i, increment); // j: Iterable created in O(1)
var c = Array.from(j); // c: [2,5,7,9,11] created in O(n)
请注意,使用可迭代合成,我们还没有创建中间结果的存储。当这些库表示他们不创建中间表示时 - 他们的意思正是这个例子中描述的内容。没有创建包含值[1,4,6,8,10]
的数据结构。
然而,当然一些中间表示。每个&#34;懒惰&#34;操作必须返回一些东西他们返回一个可迭代的。创建这些非常便宜并且与正在操作的数据的大小无关。注意,在短切融合转录中,还进行了中间表示。 compose
的结果是一个新功能。功能组合(手写或快捷融合编译器的结果)与可迭代组合非常相关。
删除中间表示的目标是性能,特别是关于内存。可重构的组合是一种实现它的强大方法,并且不需要解析和重写优化编译器的代码的开销,这些代码在运行时库中是不合适的。
APPX:
这就是lazyMap
的简单实现:
function lazyMap(iterable, mapper) {
return {
"@@iterator": function() {
var iterator = iterable["@@iterator"]();
return {
next: function() {
var step = iterator.next();
return step.done ? step : { done: false, value: mapper(step.value) }
}
};
}
};
}