【翻译】有帮助的柯里化

github地址,原文地址,译文如下

有帮助的curry

我最近使用Ramda实现函数式组合的文章涉及到一个重要的主题。为了使用Ramda的方式做某种组合,我们需要这些方法被柯里化。

柯里化是啥,能吃吗?

实际上,柯里化 是从 Haskell 来的。Haskell是第一个研究这种技术的。(是的,他们用自己的名字命名了一个函数式编程语言。不仅如此,Curry的中间初始是B,或者当然代表的是Brainf*ck

柯里化是这么一个过程,它一个本来需要多个参数的方法转化为这样一种方法,这个方法在传递更少参数之后,返回一个新方法,返回的这个新方法会等待获取余下的参数。

基本的例子看起来像这样,这是一个普通的函数:

// 没有柯里化的版本

var formatName1 = function(first, middle, last) {
  return first + middle + ' ' + last;
}

formatName1('John', 'Paul', 'Jones');
// => 'John Paul Jones' // (呃, 是音乐家或者海军上将?)

formatName1('John', 'Paul')

// => 'John Paul undefined'

但是一个柯里化的版本就更加有用了

// 柯里化的版本
var formatName2 = R.curry(function(first, middle, last) {
  return first + ' ' + middel + ' ' + last;
});

formatName2('John', 'Paul', 'Jones');
// => 'John Paul Jones' (肯定是个音乐家)

var jp = formatName2('John', 'Paul');
// => 返回一个方法

jp('Jones');
// => 'John Paul Jones' (maybe this one's the admiral)

jp('Stevens');
//=> 'John Paul Stevens' (the Supreme Court Justice)

jp('Pontiff');
//=> 'John Paul Pontiff' (ok, so I cheated.)

jp('Ziller');
//=> 'John Paul Ziller' (magician, a wee bit fictional)

jp('Georgeandringo');
//=> 'John Paul Georgeandringo' (rockers)

或者

['Jones', 'Stevens', 'Eiller'].map(jp);
//=> ['John Paul Jones', 'John Paul Stevens', 'John Paul Ziller']

你也可以在多次这样传递

var james = formatName2('James');
// => return a function

james('Byron', 'Dean');
// => 'James Byron Dean'(rebel)

var je = james('Earl');
// 也会返回一个方法
je('Carter');
// => 'James Earl Carter'(president)
je('Jones');
// => 'James Earl Jones' (actor, Vader)

有些人会认为我们在做的应该叫"部分应用"更加合适,并且"柯里化"应该只是用在这种情况:返回的方法只接收一个参数,其他每个参数都使用一个独立的新方法处理直到所有必须的参数都被提供。这些人可以继续这样迷信(意译)。

补充: 其实这里就是使用闭包给方法绑定了数据,通过返回的函数在之后的调用中复用之前绑定的数据。这里使用的例子是一个经典的场景,叫做 惰性求值

无聊!对我有什么帮助吗?

下面是个更有意义的例子。如果你想求一个数字集合的和,你可以这么做:

// 普通JS
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); // => 15

在 Ramda里面, totalsum 非常相似。亦可以这样定义 sum:

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

但是因为 reduce 是一个柯里化过的方法,但你不传最后一个参数的时候,就像在上面你自己定义的 total 函数一样:

// 在 Ramda 里
var total = R.reduce(add, 0); // 返回一个柯里化之后的方法

你仅仅拿到一个方法你可以这样调用:

var sum = total(numbers); // => 15

再次注意方法的定义和方法应用于数据上是多么相似

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

不在乎:我又不是个数学极客

所以你是一个web开发,是吧?你需要发一个AJAX请求到服务器?你需要使用 Promise?(起码我希望是这样)你需要操作返回的数据,过滤它,取得子集?或者你做服务器端开发?你异步查询SQL数据库,并且操作这些结果?

我能建议你做的最好的事情是你可以看看前端开发专家Jackson的精彩文章 为什么柯里化有帮助?。这是我看过最精彩的文章。如果你是个通过视频学习者,花半个小时看下Dr. Boolean的视频 Underscore,你做错了(不要在意标题,他没有花费他多时间实际吐槽工具库)。

真的去做。看看这些,它们解释得比我好,你已经看出来我多啰嗦了,巴拉巴拉。如果你已经看过这些,你可以跳过余下的章节。他们已经比我说得更好了。

该说的我都说了。



想象我们要获取像下面这样的数据

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 接收一个 membername 参数,然后从服务器(后者其他地方)获取数据,选择没有完成的会员,返回他们的ids,priorities, titles和due dates,根据due date排序。实际上,它返回一个应该在排序列表里面resolve的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"}
]

好啦,我们开始,下面的code看起来熟悉吗?

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方法都被柯里化了。(实际上,所有超过一个参数的Ramda方法都被柯里化了,只有一些必要的例外。)在所有的情况下,柯里化让组合这些方法到一个优雅的块中变得简单。

让我们看看发生了什么。

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

所以当我们通过 usernamemembername 调用它的时候(后面的参数被作为参数传递给我们的方法),柯里化给我们返回了一个新方法,等价于

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

membername的值被绑定为传给我们的值。

这个方法之后被传给 filter

Ramda的 filter 方法就像在 Array.prototype 上的原生filter方法,但是函数签名是

ramda.filter = curry(function(predicate, list) {

});

所以我们已经柯里化过了,只需要传递 predicate 参数,并且前一步的tasks列表不需要。(我告诉过你所有的东西已经被柯里化了,没有吗?)

我们对于 propEq('complete', true) -> rejectpropEq('username', membername) -> 做了同样的事情。filter.Rejectfilter 是一样的,除了它会反转语义。它只保留哪些 predicate 返回 false的值。

好了,你还在读吗?我们手指都累了。(真的要学学touch-type了)你真的不需要我来解释最后两行了,对吧?真的?你确定?好吧,好吧!是的!...是的,我说过我会的!

所以接下来我们看

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

pick 接收一个属性名数组和一个对象,并且返回一个新的对象,这个对象会把原来的属性拷贝过来。因为我们只传递了属性名,我们只要给它传递一个对象,就能得到一个会返回新对象的方法。这个方法被传递给 R.map 函数。跟 filter 一样,跟原生方法的版本工作方式相同,但是函数签名不同:

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

这里再多嘴一下 -- 我已经说过我很乏味 -- 这个方法已经柯里化过了,因为我们只是传递了 pick 函数(柯里化后)的输出,而不是列表,then 会调用使用tasks列表调用这个方法。

好啦,记得坐在学校教室等待下课?钟表上的分针卡住了,秒针像是被糖蜜粘住了吗?老师成天唠叨一遍又一遍同样的事情。还记得吗?然后也许就是两分钟要结束前那一刻,结束尽在眼前:哈利路亚!我想这个例子完了之后我们就到了,就剩这个了:

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

我们已经说过 get 方法了.就像这样柯里化之后,它返回一个方法,这个方法接受一个对象并且返回 dueDate 属性.我们把它传递给 sortBy 方法,这个方法接收一个列表并且基于方法返回值对这个列表元素排序.但是,等等,我们没有列表,对吧?当然没有,我们又柯里化了.但是当我们被 .then() 方法调用的时候,它会接收这个列表,把每个对象传递给 get 方法并且基于结果排序.

所以柯里化有多重要?

这个例子展示了 Ramda 的工具方法跟 Ramda 柯里化版本.也许柯里化并没有那么重要.让我们来在没有柯里化的情况下重写:

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 的工具方法有些,额,功用,甚至在柯里化的版本中.但是我不认为它比下面的版本可读性更好:

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 其中一个目标就是要通过方便的包装来提供所有这些.

时间: 2024-10-31 21:58:22

【翻译】有帮助的柯里化的相关文章

Swift函数柯里化(Currying)简谈

大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 下面简单说说Swift语言中的函数柯里化.简单的说就是把接收多个参数的函数划分为若干个"嵌套"的单一参数的函数. 这样说谁也听不懂,我们结合一个例子给大家简单说说. 我们需要定义一个函数A,该函数返回一个函数B,函数B创建一只大蜘蛛.为什么要间接返回大蜘蛛,因为本猫最怕大蜘蛛,所以不敢直接返回大蜘蛛 ;) 首先是蜘蛛的类: class Spider:

柯里化的前生今世(四):编译器与解释器

关于 在上一篇中,我们提到了形式语言与文法,S表达式与M表达式,同像性. 本文将开始写一个简单的解释器, 通过具体实现,我们来理解求值环境,动态作用域和静态作用域,还有闭包等概念. 当然,一篇文章来写完这些肯定是不够的,我们可以慢慢来,循序渐进. 写完了这个解释器之后,我们会增加一些新的功能. 编译器与解释器 编译器会将源代码转换成另一种语言的代码,然后在支持后一种语言的机器上执行. 而解释器则不同,它会逐行分析源代码,直接执行分析结果. 值得一提的是,编译和解释是执行代码的两种手段, 具体的语

浅析 JavaScript 中的 函数 currying 柯里化

原文:浅析 JavaScript 中的 函数 currying 柯里化 何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名). 柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果.因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程. 柯里化一个求和函数 按照分步求值,我们看一个

柯里化的前生今世(九):For Great Good

关于 上文第二~八篇中,我们学习了Racket语言,它很有代表性,是一种Lisp方言. 很多概念用Racket描述会更加简便. 我们介绍了高阶函数,词法作用域,闭包以及continuation, 这些概念对理解函数式编程来说十分重要. 然而,偏见却经常起于片面. 只学习一种语言,会让我们对事物的同一个侧面产生习惯. 事实上,我们需要多样化的角度,也需要经常更换思维方式. 这对学习新知识很有帮助, 有些时候,我们理解不了某些概念,很有可能是因为这个概念被描述的不够全面, 我们经常走到深入思考这一特

柯里化的前生今世(十二):多态性

关于 本文借用Haskell介绍了自定义类型,带参数的类型,Ad-hoc多态性,kind, 其中,带参数的类型在类型上可以做"柯里化". 1. 自定义类型 Haskell中使用data自定义类型. data Bool = True | False 其中,Bool是类型名,True和False是该类型的值. 一个值可以包含多种不同类型的字段(field),例如, data BookType = BookValue Int String 其中BookType是类型名,BookValue是值

柯里化的前生今世(一):函数面面观

关于 本文作为开篇,介绍了出场人物,并形象化的引入了高阶函数, 得到了柯里化的概念. 后续文章,会介绍高阶函数的实现方式,词法作用域和闭包,参数化类型,类型上的柯里化, 敬请期待. 如有不同的认识,或者感兴趣的点,请直接联系我,欢迎指教. 人物介绍 球星库里 库里,Stephen Curry,1988年3月14日出生于美国俄亥俄州阿克伦(Akron, Ohio), 美国职业篮球运动员,司职控球后卫,效力于NBA金州勇士队. 斯蒂芬·库里2009年通过选秀进入NBA后一直效力于勇士队,新秀赛季入选

Swift 柯里化(Currying)

在计算机科学中,柯里化(英语:Currying),又譯為卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术.这个技术由 Christopher Strachey 以逻辑学家哈斯凱爾·加里命名的,尽管它是 Moses Schönfinkel 和 戈特洛布·弗雷格 发明的. ------维基百科 Swift支持将方法柯里化,类似于批量创建某个带有固定参数的方法,就像下面这个例子,用Swift做个简单的加法运算:

柯里化的前生今世(八):尾调用与CPS

关于 在上一篇中,我们介绍了continuation的概念,还介绍了Lisp中威力强大的call/cc,它提供了first-class continuation,最后我们用call/cc实现了python中的generator和yield. call/cc赋予了我们很强的表达能力,Lisp中的异常处理机制也很人性化. 例如,Common Lisp: Condition_system, 由于call/cc可以捕捉到异常处的continuation, 我们就可以手动调用这个continuation,

浅谈JS中的bind方法与函数柯里化_javascript技巧

绑定函数bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值.不同于call和apply只是单纯地设置this的值后传参,它还会将所有传入bind()方法中的实参(第一个参数之后的参数)与this一起绑定. 关于这个特性看<JS权威指南>原文的例子: var sum = function(x,y) { return x + y }; var succ = sum.bind(null, 1); //让this指向null,其后的实参也会作为实参传入被绑定的函数sum