我怎样才能让 Underscore 表现得像 Ramda?
How can I make Underscore behave like Ramda?
两天前,我宣布 a preview release of Underscore that integrates with the new Node.js way of natively supporting ES modules.1 Yesterday, somebody responded on Twitter 并提出以下问题:
Can you do Ramda-style data last functions?
他或她指的是 Underscore 和 Ramda 之间的主要区别之一。在Underscore中,函数通常将要操作的数据作为第一个参数,而Ramda将它们作为最后一个参数:
import _ from 'underscore';
import * as R from 'ramda';
const square = x => x * x;
// Underscore
_.map([1, 2, 3], square); // [1, 4, 9]
// Ramda
R.map(square, [1, 2, 3]); // [1, 4, 9]
Ramda 中数据最后顺序背后的想法是,在进行部分应用时,数据参数通常是最后提供的。在这种情况下,将数据作为最后一个参数就不需要占位符了:
// Let's create a function that maps `square` over its argument.
// Underscore
const mapSquare = _.partial(_.map, _, square);
// Ramda with explicit partial application
const mapSquare = R.partial(R.map, [square]);
// Ramda, shorter notation through automatic currying
const mapSquare = R.map(square);
// Ramda with currying and placeholder if it were data-first
const mapSquare = R.map(R.__, square)
// Behavior in all cases
mapSquare([1, 2, 3]); // [1, 4, 9]
mapSquare([4, 5, 6]); // [16, 25, 36]
如示例所示,特别是 curried 符号使 data-last 对于此类场景具有吸引力。
为什么 Underscore 不这样做?这有几个原因,我将其放在脚注中。2 然而,让 Underscore 像 Ramda 一样运行是函数式编程中的一个有趣练习。在下面的回答中,我将展示如何仅用几行代码就可以做到这一点。
1 在撰写本文时,如果您想尝试一下,我建议从 NPM 安装 underscore@preview
。这可确保您获得最新的预览版本。我刚刚发布了一个将版本升级到 1.13.0-1 的修复程序。我将在不久的将来发布 1.13.0 underscore@latest
。
2 Underscore 不实现 data-last 或 currying 的原因:
- 当 Jeremy Ashkenas 从 DocumentCloud (together with Backbone 中提取出常见模式时,下划线诞生了。碰巧的是,data-last partial application 和 currying 都不是该应用程序中的常见模式。
- 将下划线从数据优先更改为数据最后会破坏很多代码。
- 部分应用最后提供数据不是普遍的规则;首先提供数据同样是可以想象的。因此,data-last 并没有从根本上更好,它只是做出了不同的权衡。
- 虽然柯里化很好,但它也有一些缺点:它增加了开销并修复了函数的数量(除非您使函数成为惰性函数,这会增加更多开销)。与 Ramda 相比,Underscore 更适用于可选参数和可变参数,并且更喜欢制作增加开销的功能 opt-in 而不是默认启用它们。
从字面上看这个问题,让我们从一个将数据优先函数转换为数据最后函数的函数开始:
const dataLast = f => _.restArguments(function(args) {
args.unshift(args.pop());
return f.apply(this, args);
});
const dataLastMap = dataLast(_.map);
dataLastMap(square, [1, 2, 3]); // [1, 4, 9]
我们可以将 dataLast
映射到 Underscore 上以获得整个库的最后数据版本:
const L = _.mapObject(_, dataLast);
const isOdd = x => x % 2;
L.map(square, [1, 2, 3]); // [1, 4, 9]
L.filter(isOdd, [1, 2, 3]); // [1, 3]
但是,我们可以做得更好。 Ramda 风格的柯里化也不太难实现:
const isPlaceholder = x => x === _;
function curry(f, arity = f.length, preArgs = []) {
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
const supplied = _.countBy(args, isPlaceholder)['false'];
if (supplied < arity) {
return curry(applied, arity - supplied, args);
} else {
return applied.apply(null, args);
}
});
}
只需一点额外的复杂性,我们甚至可以正确支持 this
绑定:
function curry(f, arity = f.length, preArgs = [], thisArg) {
if (!_.isUndefined(thisArg)) f = f.bind(thisArg);
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
const supplied = _.countBy(args, isPlaceholder)['false'];
if (supplied < arity) {
return curry(applied, arity - supplied, args, this);
} else {
return applied.apply(this, args);
}
});
}
柯里化本身与您是先数据还是后数据无关。这是 _.map
的柯里化版本,它仍然是数据优先的:
const curriedMap = curry(_.map);
curriedMap([1, 2, 3], square, null);
curriedMap([1, 2, 3])(square, null);
curriedMap([1, 2, 3])(square)(null);
curriedMap([1, 2, 3], square)(null);
curriedMap([1, 2, 3], _, null)(square);
curriedMap(_, _, null)([1, 2, 3], square);
curriedMap(_, _, null)(_, square)([1, 2, 3]);
curriedMap(_, square, _)(_, null)([1, 2, 3]);
// all [1, 4, 9]
请注意,我每次都必须传递 null
,因为 _.map
采用可选的第三个参数,可让您将回调绑定到上下文。这种热切的柯里化风格迫使您传递固定数量的参数。在下面的 Variation 部分,我将展示如何使用 curry
.
的惰性变体来避免这种情况
Ramda 库省略了可选的上下文参数,因此您需要将两个而不是三个参数传递给 R.map
。我们可以编写一个函数来组合 dataLast
和 curry
并且可以选择调整元数,以使 Underscore 函数的行为与它的 Ramda 函数完全一样:
const ramdaLike = (f, arity = f.length) => curry(dataLast(f), arity);
const ramdaMap = ramdaLike(_.map, 2);
ramdaMap(square, [1, 2, 3]);
ramdaMap(square)([1, 2, 3]);
ramdaMap(_, [1, 2, 3])(square);
// all [1, 4, 9]
将其映射到整个库需要一些管理才能获得令人满意的结果,但结果是对 Ramda 的惊人忠实模仿:
const arityOverrides = {
map: 2,
filter: 2,
reduce: 3,
extend: 2,
defaults: 2,
// etcetera, as desired
};
const R_ = _.extend(
// start with just passing everything through `ramdaLike`
_.mapObject(_, f => ramdaLike(f)),
// then replace a subset with arity overrides
_.mapObject(arityOverrides, (arity, name) => ramdaLike(_[name], arity)),
);
R_.identity(1); // 1
R_.map(square)([1, 2, 3]); // [1, 4, 9]
R_.filter(isOdd)([1, 2, 3]); // [1, 3]
const add = (a, b) => a + b;
const sum = R_.reduce(add, 0);
sum([1, 2, 3]); // 6
变化
以引入惰性为代价,我们可以避免必须修复函数的元数。这使我们可以保留原始 Underscore 函数中的所有可选参数和可变参数,而无需始终提供它们,并且在映射库时无需对每个函数进行管理。我们从 curry
的变体开始,returns 一个惰性函数而不是一个急切的函数:
function curryLazy(f, preArgs = [], thisArg) {
if (!_.isUndefined(thisArg)) f = f.bind(thisArg);
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
if (args.length > 0) {
return curryLazy(applied, args, this);
} else {
return applied.call(this);
}
});
}
这基本上是 R.curry
,上面有一个内置的 R.thunkify
。请注意,此实现实际上比 eager 变体要简单一些。最重要的是,创建一个懒惰的、类似 Ramda 的 Underscore 端口被简化为一个优雅的单行代码:
const LR_ = _.mapObject(_, _.compose(curryLazy, dataLast));
我们现在可以根据需要向每个函数传递任意数量的参数。我们只需要附加一个不带参数的额外调用以强制评估:
LR_.identity(1)(); // 1
LR_.map([1, 2, 3])(); // [1, 2, 3]
LR_.map(square)([1, 2, 3])(); // [1, 4, 9]
LR_.map(_, [1, 2, 3])(square)(); // [1, 4, 9]
LR_.map(Math.sqrt)(Math)([1, 4, 9])(); // [1, 2, 3]
LR_.filter([1, false, , '', 'yes'])(); // [1, 'yes']
LR_.filter(isOdd)([1, 2, 3])(); // [1, 3]
LR_.filter(_, [1, 2, 3])(isOdd)(); // [1, 3]
LR_.filter(window.confirm)(window)([1, 2, 3])(); // depends on user
LR_.extend({a: 1})({a: 2, b: 3})();
// {a: 1, b: 3}
LR_.extend({a: 1})({a: 2, b: 3})({a: 4})({b: 5, c: 6})();
// {a: 4, b: 3, c: 6}
这用一些对 Ramda 的忠诚来换取对 Underscore 的忠诚。在我看来,它是两全其美:像 Ramda 中的数据最后柯里化,具有 Underscore 的所有参数灵活性。
参考文献:
两天前,我宣布 a preview release of Underscore that integrates with the new Node.js way of natively supporting ES modules.1 Yesterday, somebody responded on Twitter 并提出以下问题:
Can you do Ramda-style data last functions?
他或她指的是 Underscore 和 Ramda 之间的主要区别之一。在Underscore中,函数通常将要操作的数据作为第一个参数,而Ramda将它们作为最后一个参数:
import _ from 'underscore';
import * as R from 'ramda';
const square = x => x * x;
// Underscore
_.map([1, 2, 3], square); // [1, 4, 9]
// Ramda
R.map(square, [1, 2, 3]); // [1, 4, 9]
Ramda 中数据最后顺序背后的想法是,在进行部分应用时,数据参数通常是最后提供的。在这种情况下,将数据作为最后一个参数就不需要占位符了:
// Let's create a function that maps `square` over its argument.
// Underscore
const mapSquare = _.partial(_.map, _, square);
// Ramda with explicit partial application
const mapSquare = R.partial(R.map, [square]);
// Ramda, shorter notation through automatic currying
const mapSquare = R.map(square);
// Ramda with currying and placeholder if it were data-first
const mapSquare = R.map(R.__, square)
// Behavior in all cases
mapSquare([1, 2, 3]); // [1, 4, 9]
mapSquare([4, 5, 6]); // [16, 25, 36]
如示例所示,特别是 curried 符号使 data-last 对于此类场景具有吸引力。
为什么 Underscore 不这样做?这有几个原因,我将其放在脚注中。2 然而,让 Underscore 像 Ramda 一样运行是函数式编程中的一个有趣练习。在下面的回答中,我将展示如何仅用几行代码就可以做到这一点。
1 在撰写本文时,如果您想尝试一下,我建议从 NPM 安装 underscore@preview
。这可确保您获得最新的预览版本。我刚刚发布了一个将版本升级到 1.13.0-1 的修复程序。我将在不久的将来发布 1.13.0 underscore@latest
。
2 Underscore 不实现 data-last 或 currying 的原因:
- 当 Jeremy Ashkenas 从 DocumentCloud (together with Backbone 中提取出常见模式时,下划线诞生了。碰巧的是,data-last partial application 和 currying 都不是该应用程序中的常见模式。
- 将下划线从数据优先更改为数据最后会破坏很多代码。
- 部分应用最后提供数据不是普遍的规则;首先提供数据同样是可以想象的。因此,data-last 并没有从根本上更好,它只是做出了不同的权衡。
- 虽然柯里化很好,但它也有一些缺点:它增加了开销并修复了函数的数量(除非您使函数成为惰性函数,这会增加更多开销)。与 Ramda 相比,Underscore 更适用于可选参数和可变参数,并且更喜欢制作增加开销的功能 opt-in 而不是默认启用它们。
从字面上看这个问题,让我们从一个将数据优先函数转换为数据最后函数的函数开始:
const dataLast = f => _.restArguments(function(args) {
args.unshift(args.pop());
return f.apply(this, args);
});
const dataLastMap = dataLast(_.map);
dataLastMap(square, [1, 2, 3]); // [1, 4, 9]
我们可以将 dataLast
映射到 Underscore 上以获得整个库的最后数据版本:
const L = _.mapObject(_, dataLast);
const isOdd = x => x % 2;
L.map(square, [1, 2, 3]); // [1, 4, 9]
L.filter(isOdd, [1, 2, 3]); // [1, 3]
但是,我们可以做得更好。 Ramda 风格的柯里化也不太难实现:
const isPlaceholder = x => x === _;
function curry(f, arity = f.length, preArgs = []) {
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
const supplied = _.countBy(args, isPlaceholder)['false'];
if (supplied < arity) {
return curry(applied, arity - supplied, args);
} else {
return applied.apply(null, args);
}
});
}
只需一点额外的复杂性,我们甚至可以正确支持 this
绑定:
function curry(f, arity = f.length, preArgs = [], thisArg) {
if (!_.isUndefined(thisArg)) f = f.bind(thisArg);
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
const supplied = _.countBy(args, isPlaceholder)['false'];
if (supplied < arity) {
return curry(applied, arity - supplied, args, this);
} else {
return applied.apply(this, args);
}
});
}
柯里化本身与您是先数据还是后数据无关。这是 _.map
的柯里化版本,它仍然是数据优先的:
const curriedMap = curry(_.map);
curriedMap([1, 2, 3], square, null);
curriedMap([1, 2, 3])(square, null);
curriedMap([1, 2, 3])(square)(null);
curriedMap([1, 2, 3], square)(null);
curriedMap([1, 2, 3], _, null)(square);
curriedMap(_, _, null)([1, 2, 3], square);
curriedMap(_, _, null)(_, square)([1, 2, 3]);
curriedMap(_, square, _)(_, null)([1, 2, 3]);
// all [1, 4, 9]
请注意,我每次都必须传递 null
,因为 _.map
采用可选的第三个参数,可让您将回调绑定到上下文。这种热切的柯里化风格迫使您传递固定数量的参数。在下面的 Variation 部分,我将展示如何使用 curry
.
Ramda 库省略了可选的上下文参数,因此您需要将两个而不是三个参数传递给 R.map
。我们可以编写一个函数来组合 dataLast
和 curry
并且可以选择调整元数,以使 Underscore 函数的行为与它的 Ramda 函数完全一样:
const ramdaLike = (f, arity = f.length) => curry(dataLast(f), arity);
const ramdaMap = ramdaLike(_.map, 2);
ramdaMap(square, [1, 2, 3]);
ramdaMap(square)([1, 2, 3]);
ramdaMap(_, [1, 2, 3])(square);
// all [1, 4, 9]
将其映射到整个库需要一些管理才能获得令人满意的结果,但结果是对 Ramda 的惊人忠实模仿:
const arityOverrides = {
map: 2,
filter: 2,
reduce: 3,
extend: 2,
defaults: 2,
// etcetera, as desired
};
const R_ = _.extend(
// start with just passing everything through `ramdaLike`
_.mapObject(_, f => ramdaLike(f)),
// then replace a subset with arity overrides
_.mapObject(arityOverrides, (arity, name) => ramdaLike(_[name], arity)),
);
R_.identity(1); // 1
R_.map(square)([1, 2, 3]); // [1, 4, 9]
R_.filter(isOdd)([1, 2, 3]); // [1, 3]
const add = (a, b) => a + b;
const sum = R_.reduce(add, 0);
sum([1, 2, 3]); // 6
变化
以引入惰性为代价,我们可以避免必须修复函数的元数。这使我们可以保留原始 Underscore 函数中的所有可选参数和可变参数,而无需始终提供它们,并且在映射库时无需对每个函数进行管理。我们从 curry
的变体开始,returns 一个惰性函数而不是一个急切的函数:
function curryLazy(f, preArgs = [], thisArg) {
if (!_.isUndefined(thisArg)) f = f.bind(thisArg);
const applied = _.partial.apply(null, [f].concat(preArgs));
return _.restArguments(function(args) {
if (args.length > 0) {
return curryLazy(applied, args, this);
} else {
return applied.call(this);
}
});
}
这基本上是 R.curry
,上面有一个内置的 R.thunkify
。请注意,此实现实际上比 eager 变体要简单一些。最重要的是,创建一个懒惰的、类似 Ramda 的 Underscore 端口被简化为一个优雅的单行代码:
const LR_ = _.mapObject(_, _.compose(curryLazy, dataLast));
我们现在可以根据需要向每个函数传递任意数量的参数。我们只需要附加一个不带参数的额外调用以强制评估:
LR_.identity(1)(); // 1
LR_.map([1, 2, 3])(); // [1, 2, 3]
LR_.map(square)([1, 2, 3])(); // [1, 4, 9]
LR_.map(_, [1, 2, 3])(square)(); // [1, 4, 9]
LR_.map(Math.sqrt)(Math)([1, 4, 9])(); // [1, 2, 3]
LR_.filter([1, false, , '', 'yes'])(); // [1, 'yes']
LR_.filter(isOdd)([1, 2, 3])(); // [1, 3]
LR_.filter(_, [1, 2, 3])(isOdd)(); // [1, 3]
LR_.filter(window.confirm)(window)([1, 2, 3])(); // depends on user
LR_.extend({a: 1})({a: 2, b: 3})();
// {a: 1, b: 3}
LR_.extend({a: 1})({a: 2, b: 3})({a: 4})({b: 5, c: 6})();
// {a: 4, b: 3, c: 6}
这用一些对 Ramda 的忠诚来换取对 Underscore 的忠诚。在我看来,它是两全其美:像 Ramda 中的数据最后柯里化,具有 Underscore 的所有参数灵活性。
参考文献: