如何解决我怎样才能让 Underscore 表现得像 Ramda?
两天前,我宣布了 a preview release of Underscore that integrates with the new Node.js way of natively supporting ES modules.1 昨天,somebody responded on Twitter 提出以下问题:
你能做 Ramda 风格的 data last 函数吗?
他或她指的是 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,3]); // [1,9]
Ramda 中 data-last 顺序背后的想法是,在进行部分应用时,data 参数通常最后提供。在这种情况下,将数据作为最后一个参数消除了对占位符的需要:
// 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,9]
mapSquare([4,5,6]); // [16,25,36]
如示例所示,特别是柯里化符号使 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(连同 Backbone)中提取出常见模式时,Underscore 诞生了。碰巧的是,数据最后部分应用程序和柯里化都不是该应用程序中的常见模式。
- 将 Underscore 从数据优先更改为数据最后会破坏很多代码。
- 在部分应用中最后提供数据并不是普遍规则;首先提供数据同样可以想象。因此,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,9]
L.filter(isOdd,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,preArgs = [],thisArg) {
if (!_.isUndefined(thisArg)) f = f.bind(thisArg);
const applied = _.partial.apply(null,args,this);
} else {
return applied.apply(this,args);
}
});
}
柯里化本身与您是先执行数据还是最后执行数据无关。这是 _.map
的柯里化版本,它仍然是数据优先:
const curriedMap = curry(_.map);
curriedMap([1,3],square,null);
curriedMap([1,3])(square,3])(square)(null);
curriedMap([1,square)(null);
curriedMap([1,_,null)(square);
curriedMap(_,null)([1,square);
curriedMap(_,null)(_,square)([1,3]);
curriedMap(_,_)(_,3]);
// all [1,9]
请注意,我每次都必须传递 null
,因为 _.map
需要一个可选的第三个参数,可让您将回调绑定到上下文。这种急切的柯里化风格迫使您传递固定数量的参数。在下面的变体部分,我将展示如何使用 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,3]);
ramdaMap(square)([1,3]);
ramdaMap(_,3])(square);
// all [1,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,3]); // [1,9]
R_.filter(isOdd)([1,3]
const add = (a,b) => a + b;
const sum = R_.reduce(add,0);
sum([1,3]); // 6
变化
以引入惰性为代价,我们可以避免必须修复函数的元数。这让我们可以保留原始 Underscore 函数中的所有可选参数和可变参数,而无需始终提供它们,并且在映射库时无需对每个函数进行管理。我们从 curry
的一个变体开始,它返回一个惰性函数而不是一个急切函数:
function curryLazy(f,[f].concat(preArgs));
return _.restArguments(function(args) {
if (args.length > 0) {
return curryLazy(applied,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,3])(); // [1,3]
LR_.map(square)([1,3])(); // [1,9]
LR_.map(_,3])(square)(); // [1,9]
LR_.map(Math.sqrt)(Math)([1,9])(); // [1,3]
LR_.filter([1,false,'','yes'])(); // [1,'yes']
LR_.filter(isOdd)([1,3]
LR_.filter(_,3])(isOdd)(); // [1,3]
LR_.filter(window.confirm)(window)([1,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 的所有参数灵活性。
参考文献:
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。