哎呀吆博客

大前端爱好与探索者-Jerry

Favoring Curry(爱上柯里化)-译文

JavaScript 0 评

偶尔见他们分享的一篇关于函数式编程的外国博文,翻译并学习一下。
这篇博客早在5年前就已经写完了。尽管有些代码还能正常运行,但有些部分已经失效了(与此同时,你需要用groupBy替换partition,用useWith(…, […])替换use(...).over()。我不打算尝试着去更新代码了,因为现在网上有很多地方可以去学Ramda.js的基础。这篇博客仍然能体现出很多Ramda.js(以下简称Ramda)基础知识,并且很多代码都很运行正常,但我并不担保它不会出现什么问题。
我最近的文章轻描谈写讨论了Ramda中复合函数这一重要话题。为了能够更好的去区分复合函数我们经常会用Ramda中的一些函数,我们需要这些函数能被当作参数进行传递。
将函数作为参数?像食物里面添加香料?要怎么传?放在哪?
事实上,curry是由Haskell Curry命名的,他是第一个研究这种技术的人(显而易见,这是以他自己的名字来命名的)。
Currying(柯里化)就是将多个函数作为参数传入并返回一个新函数的过程。当传递的参数不足时,会返回一个等待传参的新函数。
普通函数就像下面这样:

var formatName1 = function(first, middle, last) {
Return first + ‘ ’ + middle + ‘ ’ + last; 
};
formatName1(‘John’, ‘Paul’, ‘Jones’); // John Paul Jones
formatName1(‘John’, ‘Paul’); // John Paul undefied

将函数作为参数的版本:

Var formatNames2 = R.curry(function(first, middle, last) {
Return first + ‘ ’ + middle + ‘ ’ + last;
});
formatNames2(‘John’, ‘Paul’, ‘Jones’); // John Paul Jones
var jp = formatNames2(‘John’, ‘Paul’); // 这里返回的是一个等待进一步传参的函数
jp(‘Jones’) // John Paul Jones
jp(‘Strevens’) // John Paul Strevens
jp(‘Pontiff’) // John Paul Pontiff
jp(‘Ziller’) // John Paul Ziller
jp(‘Georgeandringo’) // John Paul Georgeandringo

或者这么来

[‘Jones’, ‘Stevens’, ‘Ziller’].map(jp); // ['John Paul Jones', 'John Paul Stevens', 'John Paul Ziller']

你也可以一次传入多个:

var james = formatNames2(‘James’); // 返回一个函数
james(‘Byron’, ‘Dean’); // James Byron Dean
var = je = james(‘Earl’); 这样也能返回一个待接受参数的函数
je(‘’Carter); // James Earl Carter
je(‘Jones’); // James Earl Jones

有些人会坚持认为我们正在做的事情更应该被称为“部分应用”,并且“柯里化”应为结果函数保留能够传入一个参数的情况,每个参数解析为一个单独的新函数,直到所有必需参数都已经传入。他们可以随时继续坚持他们的观点。
困惑?它到底对我们有什么用呢?
接下来给一个更有实际意义的例子,假如你想计算一个数组里面所有数字的和,你可以这么做:

var add = function(a, b) {return a + b;};
var numbers = [1, 2, 3, 4, 5];
var sum = numbers.reduce(add, 0);  // 15

而且如果你想写一个通用型的函数来计算任意由数字组成的数组,你可以这么写:

var total = function(list) {
return list.reduce(add, 0);
};
var sum = total(numbers);

在Ramda中,totalsum非常相似。你可以这么定义sum。

var sum = R.reduce(add, 0, numbers); // 15

但是因为reduce也是一个高阶函数,在定义total时,当你跳过最后一个参数后:

var total = R.reduce(add, 0); // 返回一个函数

这样你就获得了一个你可以调用的函数:

var sum = total(numbers); // 15

再次对比以下函数定义和接收了数据参数的函数的相似与区别之处。

var total = R.reduce(add, 0); // 返回一个待传参的函数
var sum = R.reduce(add, 0, numbers) // 返回结果 15

不用在意: 我不是一个数学极客!
那么,你是一个web开发人员吗?有没有使用AJAX从服务器上请求数据?我希望你正在使用Promise。你是否对数据进行过诸如筛选、抽取等操作?或者说你是一个后台开发人员?你异步查询过no-SQL数据库吗,操作过这些数据吗?
我建议你最好去看一下Hugh FD Jackson的优秀博文:Why Curry Helps(http://hughfdjackson.com/javascript/why-curry-helps/),这是我看过的相关话题最好的一篇博文了。如果你更喜欢通过视频来学习的话,那么你可以花半小时的事件来观看Dr. Boolean的Hey Underscore, You’re Doing It Wrong(http://www.youtube.com/watch?v=m3svKOdZijA)(不用介意这个标题,他并没有花太多的时间在这里赘述文档)
说句实话,看了这些你会觉得它比我解释的更好;你可以看到我讲的过于详细、话多、啰嗦并且从头到尾唠叨没完。如果你看过这些,那么你可以直接跳过这个部分。毕竟他们讲的比我好。
假设我们期望获得如下格式的数据:

var data = {
    result: "SUCCESS",
    interfaceVersion: "1.0.3",
    requested: "10/17/2013 15:31:20",
    lastUpdated: "10/16/2013 10:52:39",
    tasks: [
        {id: 104, complete: false,            priority: "high",
                  dueDate: "2013-11-29",      username: "Scott",
                  title: "Do something",      created: "9/22/2013"},
        {id: 105, complete: false,            priority: "medium",
                  dueDate: "2013-11-22",      username: "Lena",
                  title: "Do something else", created: "9/22/2013"},
        {id: 107, complete: true,             priority: "high",
                  dueDate: "2013-11-22",      username: "Mike",
                  title: "Fix the foo",       created: "9/22/2013"},
        {id: 108, complete: false,            priority: "low",
                  dueDate: "2013-11-15",      username: "Punam",
                  title: "Adjust the bar",    created: "9/25/2013"},
        {id: 110, complete: false,            priority: "medium",
                  dueDate: "2013-11-15",      username: "Scott",
                  title: "Rename everything", created: "10/2/2013"},
        {id: 112, complete: true,             priority: "high",
                  dueDate: "2013-11-27",      username: "Lena",
                  title: "Alter all quuxes",  created: "10/5/2013"}
        // , ...
    ]
};

然后我们现在需要创建一个名为getIncompleteTaskSummaries的方法,这个方法可以接收一个成员名称的参数,根据这个参数向服务器(或者其他什么地方)来请求相关数据,为该成员选择出tasks属性对应的数据。其中包含id、priority、deuDate,并且根据deuDate进行排序。事实上,请求返回来的是一个Promise对象,我们需要将其处理成我们需要的数据列表。
假如你通过传入“Scott”来调用getIncompleteTaskSummaries,它可能会返回如下数据:

[
    {id: 110, title: "Rename everything", 
        dueDate: "2013-11-15", priority: "medium"},
    {id: 104, title: "Do something", 
        dueDate: "2013-11-29", priority: "high"}
]

好的,我们继续,这些代码我们看起来是不是很眼熟?

getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(function(data) {
            return data.tasks;
        })
        .then(function(tasks) {
            var results = [];
            for (var i = 0, len = tasks.length; i < len; i++) {
                if (tasks[i].username == membername) {
                    results.push(tasks[i]);
                }
            }
            return results;
        })
        .then(function(tasks) {
            var results = [];
            for (var i = 0, len = tasks.length; i < len; i++) {
                if (!tasks[i].complete) {
                    results.push(tasks[i]);
                }
            }
            return results;
        })
        .then(function(tasks) {
            var results = [], task;
            for (var i = 0, len = tasks.length; i < len; i++) {
                task = tasks[i];
                results.push({
                    id: task.id,
                    dueDate: task.dueDate,
                    title: task.title,
                    priority: task.priority
                })
            }
            return results;
        })
        .then(function(tasks) {
            tasks.sort(function(first, second) {
                var a = first.dueDate, b = second.dueDate;
                return a < b ? -1 : a > b ? 1 : 0;
            });
            return tasks;
        });
};

现在如果代码像这样的话会不会简洁很多?

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.get('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.reject(R.propEq('complete', true)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.get('dueDate')));
};

如果你像我这么认为的话,那么柯里化可以帮你做到这些。在这个版块里提到的所有关于Ramda.js中的方法都是复合函数(事实上,我们用到的Ramda.js中的方法中除了一些必要的例外,大部分都可以传入超过一个参数)。在每个例子中,柯里化能够很容易构造出一个更加出色、优美的代码块。
来看一看它们中的原理。
get(众所周知的还有prop)是这样被定义的:

ramda.get = curry(function(name, obj) {
     return obj[name];
 });

但是当我们调用它的时候,我们仅传入了第一个参数:name。我们之前讨论过,这就意味着我们将会获得一个等待传入通过第一个then返回出来的obj参数。也就是说:

.then(R.get('tasks'))
可以被当作是下面代码的简写
.then(function(data) {
    return data.tasks;
});

接下来是propEq,它是这样被定义的:

ramda.propEq = curry(function(name, val, obj) {
    return obj[name] === val;
});

这样的话,我们可以通过传入“username”和“membername”(这些单词是作为我们方法的参数被传入),这个柯里化函数将会返回一个相当于如下的一个函数:

function(obj) {
    return obj['username'] === membername;
}

其中membername的值绑定到传递给我们的值上。
这个方法接下里便会进入到filter
Ramda.js的filter的工作原理更像是Array.prototype上的原生filter,它是这么实现的:

ramda.filter = curry(function(predicate, list) { /* ... */ });

这样我们再次实现了复合函数,并没用传入上一返回出来的tasks中的列表数据,而是仅仅传入了predicate(我是不是说过一切皆可被柯里化)。
同样的我们用propEq('username', membername) -> filter来实现propEq('complete', true) -> reject。Reject就像filter一样期望知识颠倒了感觉。它只是保留了返回false的predicate。
接下来我们看看这个:

R.pick(['id', 'dueDate', 'title', 'priority']);

pick方法接收一个包含多个属性名的数组和一个对象,并且返回一个从原对象中拷贝的包含这些属性的新对象。但是,你看,我们再次使用了复合函数。我们只是传入了属性名列表,我们获得了一个能够根据我们提供的对象返回一个新对象的一个方法。这个方法被归为R.map。就像filter,它的工作原理更像一个原生的数组原型版本,只不过又包含了自己的一些特性。

ramda.map = curry(function(fn, list) { /* ... */ });

再次说一下——我已经说过我很啰嗦的——这是一个复合函数。因为我们仅提供了来自pick(复合函数)输出的函数。而不是一个列表。then将使用任务列表调用它。

好吧,你还记得坐在学校,等待放学的时候吗?时钟上的分针就像卡住了一样不动,秒针在一点点的缓慢转动?老师在一遍又一遍的重复同样的事情,还记得这些吗?可能就是结束前的两分钟那段时间,当到了最后时刻:我们大声欢呼!我认为现在我们就是这样。这是最后一个例子了:

.then(R.sortBy(R.get('dueDate')));

我们已经讲过get了。复合函数就像这样,它返回一个这样的方法,当你传入一个对象,它会返回对应的dueDate属性。我们再将这些返回值交给sortBy来处理,sortBy会使用一个列表之类的函数,会根据函数的返回值对列表进行排序。但是,等等,我们是不是没有列表格式的数据?当然没有。我们又使用了复合函数。但是当我们被.then()调用时,通过将每个对象传递给get,并且基于这个结果进行排序,最后它将接收这个列表数据,
所以柯里化是多么的重要?
接下来这个例子是示例Ramda.js依赖柯里化来使用函数的方面。可能柯里化并不是真的那么重要。我们先来试试不使用柯里化进行重写:

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(function(data) {
            return R.get('tasks', data)
        })
        .then(function(tasks) {
            return R.filter(function(task) {
                return R.propEq('username', membername, task)
            }, tasks)
         })
        .then(function(tasks) {
            return R.reject(function(task) {
                return R.propEq('complete', true, task);
            }, tasks)
        })
        .then(function(tasks) {
            return R.map(function(task) {
                return R.pick(['id', 'dueDate', 'title', 'priority'], task);
            }, tasks);
        })
        .then(function(abbreviatedTasks) {
            return R.sortBy(function(abbrTask) {
                return R.get('dueDate', abbrTask);
            }, abbreviatedTasks);
        });
};

下面的代码我觉得跟上面是等价的,而且更好。尽管不使用柯里化,Ramda.js的函数还是比较实用。但是我不认为它会比这段代码更加可读:

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.get('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.reject(R.propEq('complete', true)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.get('dueDate')));
};

这就是我们为什么要使用柯里化的原因。
你也可以读以下的其他文章介绍:
Why Curry Helps(http://hughfdjackson.com/javascript/why-curry-helps/), Hugh FD Jackson
Hey Underscore, You’re Doing It Wrong(http://www.youtube.com/watch?v=m3svKOdZijA), Dr. Boolean, aka Brian Lonsdorf

Put callback first for elegance(https://glebbahmutov.com/blog/put-callback-first-for-elegance/), Gleb Bahmutov

原文地址:https://fr.umio.us/favoring-curry/

上一篇

发表评论

电子邮件地址不会被公开。 必填项已用*标注