为什么柯里化是有用的
程序员的白日梦就是写一次代码,然后能够不断的复用它同时不会产生副作用。它善于表达因为你以需要表达的方式来写的,它是重用因为...呃,你在重用,你还有什么更多的要求吗?
Curry (the5fire注:这是原文作者写的一个柯里化的包)能帮助你。
什么是柯里化,以及为什么它如此美味?
在JavaScript中正常的函数调用是这样的:
var add = function(a, b){ return a + b }
add(1, 2) //= 3
一个函数需要一定数量的参数,然后返回一个值。我可以通过更少的参数(返回奇怪的结果)或者更多的参数(那些额外的参数会被忽略)调用它:
add(1, 2, 'IGNORE ME') //= 3
add(1) //= NaN
一个柯里化的函数是这样——多个参数被描述为一系列的单个参数的函数。比如说,柯里化的加法函数将会是这样:
var curry = require('curry')
var add = curry(function(a, b){ return a + b })
var add100 = add(100)
add100(1) //= 101
柯里化的函数接受多个参数时将会写成这样:
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1)(2)(3) //= 6
因为这样写在JavaScript语法里比较丑陋,因此curry允许你一次传递多个参数来调用函数:
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1, 2, 3) //= 6
sum3(1)(2, 3) //= 6
sum3(1, 2)(3) //= 6
那么...还有什么?
如果你不习惯使用那些把柯里化函数作为日常生活一部分的编程语言(想到Haskell),这可能对于我们来说没有显著的优势。在我看来,两个主要的优势就是:
- 小的代码段可以被轻松配置和复用,并且不会带来杂乱;
- 函数的使用会贯穿始终。
一些小片段
让我们来看一个明显的例子;遍历映射一个集合来获取每个成员的id属性:
var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
objects.map(function(o){ return o.id })
如果你在思考第二行代码实现的逻辑,让我来告诉你:
MAP over OBJECTS to get IDS
在这一行中有很多不相关的实现;以函数式编程的定义的形式。我们来整理一下:
var get = curry(function(property, object){ return object[property] })
objects.map(get('id')) //= [1, 2, 3]
现在我们来讨论下这个操作(遍历映射这些对象,然后拿到他们的id)的真实逻辑。嘭!我们在get函数中创建了一个函数,这个函数能够被部分配置。
如果我们打算复用这个“从对象列表中获取id”的函数怎么做呢,想一想?让我们用幼稚的方式来做:
var getIDs = function(objects){
return objects.map(get('id'))
}
getIDs(objects) //= [1, 2, 3]
呃,我们似乎从之前的优雅和简洁又回到了混乱。针对这种情况我们能做些什么?啊——如果映射(map)可以被部分的配置到函数上,而不是集合上呢?
var map = curry(function(fn, value){ return value.map(fn) })
var getIDs = map(get('id'))
getIDs(objects) //= [1, 2, 3]
我们开始看到,如果我们基础的构建是基于柯里化函数的,我们可以很容易基于他们创建新的功能函数。甚至更令人兴奋的是,这个代码的读起来像是你所熟悉领域的逻辑。
彻底函数式
这种方式另外一个优势就是鼓励创建函数;而不是方法。虽然方法是个美好的东西——允许多台,并且代码可读性好——但它们不总是工作需要的工具,比如在重度异步的代码中。 Another advantage of this approach is that it encourages the creation of functions; rather than of methods. While methods are beautiful things - allowing polymorphism, and very readable code - they aren’t always the tool for the job, such as in heavily async code.
在这个玩具示例中,让我们从服务器端“拿”一些数据,然后通过有用的方式来处理它。数据看起来像下面这样:
{
"user": "hughfdjackson",
"posts": [
{ "title": "why curry?", "contents": "..." },
{ "title": "prototypes: the short(est possible) story", "contents": "..." }
]
}
你的任务就是从用户的文章中拿到所有的标题。来吧,开始吧!
fetchFromServer()
.then(JSON.parse)
.then(function(data){ return data.posts })
.then(function(posts){
return posts.map(function(post){ return post.title })
})
好吧,这不公平,我催促你了。(另外,我代你写了这段代码 —— 或许你可以写的更加优雅,我跑题了)。
因为promises的链式调用(或者如果你倾向于用,callback)从根本上可以跟函数一起工作,当刚从服务器端获取到数据,在看起来(感觉起来)杂乱的数据没有经过处理之前,你不能轻松的遍历并映射它。 (the5fire注:这段不太好理解,原文: Since chains of promises (or, if you prefer, callbacks) fundamentally work with functions, you can’t easily just map over an value when one is available from the server without first wrapping it up in all that visual (and mental) clutter.)
让我们再次开始,这次我们使用我们已经定义好的一些工具:
fetchFromServer()
.then(JSON.parse)
.then(get('posts'))
.then(map(get('title')))
这是少量逻辑,易表达的;如果没有柯里化函数我们几乎不能达到这样的目的。
长话短说
curry给了你相当诱人的表达能力。
我建议你把它用起来,并且熟悉它。如果你已经熟悉了这些概念,我怀疑你将会发现这些API直接了当并且令人满意。如果不是的话,你就要靠你自己和同事去考虑它了。
译者注: 这篇翻译主要是为了更深的理解这篇关于柯里化的文章,捎带着也练习一下英语翻译 关于语法错误欢迎拍砖 map over 跪求怎么翻译合适?
作者:Hugh FD Jackson 原标题: 《Why Curry Helps》 原文地址:https://hughfdjackson.com/javascript/why-curry-helps/ 译者:the5fire