繁体   English   中英

如何让 Underscore 表现得像 Ramda?

[英]How can I make Underscore behave like Ramda?

两天前,我宣布了 Underscore 的预览版,它集成了新的 Node.js 原生支持 ES 模块的方式 1昨天,有人在 Twitter 上回复了以下问题:

你能做 Ramda 风格的数据最后功能吗?

他或她指的是 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 中 data-last order 背后的想法是,在进行部分应用时,通常最后提供 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, 2, 3]);  // [1, 4, 9]
mapSquare([4, 5, 6]);  // [16, 25, 36]

如示例所示,尤其是 curried 表示法使 data-last 对此类场景具有吸引力。

为什么下划线不这样做? 有几个原因,我把它放在脚注中。 2然而,让 Underscore 像 Ramda 一样运行是函数式编程中的一项有趣的练习。 在下面的回答中,我将展示如何用几行代码来做到这一点。


1在撰写本文时,如果您想尝试一下,我建议从 NPM 安装underscore@preview 这可确保您获得最新的预览版本。 我刚刚发布了一个将版本提升到 1.13.0-1 的修复程序。 我将在不久的将来某个时间以underscore@latest的形式发布 1.13.0。

Underscore 不实施 data-last 或 currying 的2 个原因:

  • 当 Jeremy Ashkenas 从DocumentCloud (与Backbone一起)中分解出常见模式时,Underscore 诞生了。 碰巧的是,data-last 部分应用程序和柯里化都不是该应用程序中的常见模式。
  • 将 Underscore 从 data-first 更改为 data-last 会破坏很多代码。
  • 在部分应用中最后提供数据并不是一个普遍的规则; 首先提供数据同样可以想象。 因此,data-last 并没有从根本上更好,它只是做出了不同的权衡。
  • 虽然柯里化很好,但它也有一些缺点:它增加了开销并修复了 function 的数量(除非你让 function 变得懒惰,这会增加更多开销)。 与 Ramda 相比,Underscore 更适用于可选和可变参数 arguments,并且更喜欢制作添加开销选择加入的功能,而不是默认启用它们。

从字面上理解这个问题,让我们从 function 开始,它将数据优先的 function 转换为数据后的 function:

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]

我们可以 map dataLast over Underscore 以获得整个库的 data-last 版本:

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);
        }
    });
}

Currying 本身与您是否执行 data-first 或 data-last 无关。 这是_.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采用可选的第三个参数,可以让您将回调绑定到上下文。 这种急切的柯里化风格迫使您传递固定数量的 arguments。 在下面的变体部分中,我将展示如何使用curry的惰性变体来避免这种情况。

Ramda 库省略了可选的上下文参数,因此您需要将两个而不是三个 arguments 传递给R.map 我们可以编写一个 function 组成dataLastcurry并且可以选择调整arity,以使下划线 function 的行为与其 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

变化

以引入惰性为代价,我们可以避免修复 function 的参数。 这使我们可以保留原始 Underscore 函数中的所有可选和可变参数,而无需始终提供它们,并且在映射库时无需对每个函数进行管理。 我们从返回惰性 function 而不是急切的curry变体开始:

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 端口被简化为一个优雅的 oneliner:

const LR_ = _.mapObject(_, _.compose(curryLazy, dataLast));

我们现在可以根据需要将任意数量的 arguments 传递给每个 function。 我们只需要在没有 arguments 的情况下额外调用 append 来强制评估:

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 中的 data-last currying,以及来自 Underscore 的所有参数灵活性。


参考:

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM