什么是“柯里化”?

809

我在几篇文章和博客中看到了对柯里化函数的引用,但我找不到一个好的解释(或者至少是一个有意义的解释!)

25个回答

1085

柯里化是将接收多个参数的函数拆分成接收一个参数的一系列函数。以下是 JavaScript 的一个示例:

function add (a, b) {
  return a + b;
}

add(3, 4); // returns 7

这是一个接收两个参数a和b并返回它们的和的函数。现在我们将对这个函数进行柯里化:
function add (a) {
  return function (b) {
    return a + b;
  }
}

这是一个接受一个参数a的函数,它返回一个接受另一个参数b的函数,该函数返回它们的和。

add(3)(4); // returns 7

var add3 = add(3); // returns a function

add3(4); // returns 7
  • 第一条语句返回7,就像add(3, 4)语句一样。
  • 第二条语句定义了一个名为add3的新函数,它将3加到其参数中。(这是一种被称为闭包的东西。)
  • 第三条语句使用add3操作将3添加到4中,再次产生7作为结果。

318
从实际角度来看,我应该如何运用这个概念? - Strawberry
60
@草莓,比如说你有一个数字列表 [1, 2, 3, 4, 5],你希望将其乘以任意一个数字。在 Haskell 中,我可以写 map (* 5) [1, 2, 3, 4, 5] 来将整个列表乘以 5,从而生成列表 [5, 10, 15, 20, 25] - nyson
88
我理解 map 函数的作用,但不确定我是否理解你试图向我阐述的重点。你是在说 map 函数代表柯里化的概念吗? - Strawberry
101
map 函数的第一个参数必须是一个仅接受列表中一个元素作为参数的函数。乘法作为数学概念是一种二元运算,它需要两个参数。不过,在 Haskell 中 * 是一种柯里化函数,类似于这个回答中的第二个版本的 add(* 5) 的结果是一个只接受一个参数并将其乘以 5 的函数,这使我们能够将其与 map 一起使用。 - Doval
38
标准 ML 或 Haskell 这样的函数式语言的好处是你可以轻松地获得柯里化。你可以像在其他语言中一样定义多参数函数,然后自动获得它的柯里化版本,无需自己添加大量的 lambda。这样你就可以从任何现有函数中产生只需要少数参数的新函数,而不用费心费力,这使得将它们传递给其他函数变得容易。 - Doval
显示剩余13条评论

142
在函数代数中,处理多个参数的函数(或等价于一个 N-tuple 的一个参数)有些不太优雅,但正如摩西·肖因菲克尔(以及独立地,哈斯克尔·柯里)证明的那样,这并非必需:你只需要函数接受一个参数。
那么,如何处理自然表达为 f(x,y) 的东西呢?好吧,你可以将其等效为 f(x)(y) -- 将 f(x) 视为一个函数,并将该函数应用于 y。换句话说,你只有函数接受一个参数,但其中一些函数会返回其他函数(它们也接受一个参数;-)。
像往常一样,维基百科 对此有一个不错的入门摘要,提供了许多有用的指针(可能包括关于你最喜欢的语言的指针;-),以及稍微更严谨的数学处理方法。

1
我认为和我之前的评论类似 - 我没有看到函数式语言限制函数只能接受一个参数。我错了吗? - user140327
1
@hoohoo:函数式语言通常不会限制函数只有一个参数。然而,在更低、更数学化的层面上,处理只有一个参数的函数要容易得多。(例如,在λ演算中,函数一次只接受一个参数。) - Sam DeFabbia-Kane
1
好的。那么另一个问题是,以下陈述是否正确?Lambda演算可以用作函数式编程的模型,但函数式编程不一定应用于Lambda演算。 - user140327
8
正如维基百科页面所述,大多数函数式编程语言“装饰”或“增强”了 λ演算(例如添加一些常量和数据类型),而不仅仅是“应用”它,但它并不是非常接近。顺便问一下,是什么让你觉得Haskell等语言不“限制函数只能接受单个参数”?它确实限制,尽管由于柯里化而无关紧要;例如div :: Integral a => a -> a -> a--注意那些多个箭头?"将a映射为将a映射为a的函数"是其中一种理解方式;-)。你可以使用一个(单)元组参数来定义div等函数,但这在Haskell中真的是违反惯例的。 - Alex Martelli
@Alex - 关于 Haskell 和参数计数,我没有花太多时间在 Haskell 上,而且那都是几周前的事了。所以犯这个错误很容易。 - user140327
如何使用只接受一个参数的函数来相加两个数字? - undefined

123

这里有一个具体的示例:

假设你有一个计算物体所受重力作用的函数。如果你不知道公式,可以在这里找到。这个函数将三个必要的参数作为参数输入。

现在,作为地球上的一员,您只想计算这个星球上物体的力量。在函数式语言中,您可以将地球的质量传递给函数,然后部分求值。您将得到另一个函数,它仅需要两个参数,并计算地球上物体的重力。这被称为柯里化。


3
作为一种奇特的现象,JavaScript 的 Prototype 库提供了一个“柯里化”函数,几乎完全符合您在此处所解释的内容:http://www.prototypejs.org/api/function/curry - shuckster
新的PrototypeJS curry函数链接。http://prototypejs.org/doc/latest/language/Function/prototype/curry/index.html - Richard Ayotte
11
对我来说,这听起来像是部分应用。我的理解是,如果您使用柯里化,可以创建具有单个参数的函数并将它们组合以形成更复杂的函数。我有遗漏什么吗? - neontapir
12
@neontapir是正确的。Shea所描述的不是柯里化,而是部分应用。如果一个三个参数的函数被柯里化了,并且你用f(1)来调用它,你得到的不是一个两个参数的函数,而是一个返回另一个一元函数的一元函数。柯里化函数只能接受一个参数。PrototypeJS中的curry函数也不是柯里化,它是部分应用。 - MindJuice
不要使用部分求值和柯里化。这被称为部分应用。需要柯里化来实现它。 - Will Ness
有史以来最棒的扭曲答案。 - developer_hatch

100

它可以是使用函数来创建其他函数的一种方式。

在javascript中:

let add = function(x){
  return function(y){ 
   return x + y
  };
};

这样,我们就可以像这样调用它:

let addTen = add(10);

当这段代码运行时,x 的值被传入为 10

let add = function(10){
  return function(y){
    return 10 + y 
  };
};

这意味着我们将返回此函数:

function(y) { return 10 + y };

所以,当你打电话的时候

 addTen();

你真的在打电话:

 function(y) { return 10 + y };

那么如果你这样做:

 addTen(4)

它与此相同:

function(4) { return 10 + 4} // 14

所以我们的 addTen() 总是会将传入的参数加上十。我们可以用同样的方式创建类似的函数:

let addTwo = add(2)       // addTwo(); will add two to whatever you pass in
let addSeventy = add(70)  // ... and so on...

现在显而易见的后续问题是,你为什么想要这样做?它将原本热切执行的操作x + y变成了可以被惰性步进的操作,这意味着我们至少可以做到两件事情: 1. 缓存昂贵的操作 2. 在函数式编程范式中实现抽象。

想象一下我们的柯里化函数长这样:

let doTheHardStuff = function(x) {
  let z = doSomethingComputationallyExpensive(x)
  return function (y){
    z + y
  }
}
我们可以调用这个函数一次,然后将结果传递给许多地方使用,这意味着我们只需要进行计算上昂贵的操作一次:
let finishTheJob = doTheHardStuff(10)
finishTheJob(20)
finishTheJob(30)

我们可以以类似的方式获得抽象。


26
这是我在这里看到的内在顺序过程最好的一步一步解释,也许是最好的、最有解释性的答案。 - user7125259
7
我认为相反,这不是一个好的解释。我同意它很好地解释了所提出的例子,但人们往往倾向于认为,“是的,完全清晰,但我也可以用另一种方式做同样的事情,那么柯里化有什么用呢?”换句话说,我希望它有足够的背景或解释来阐明柯里化的工作原理,以及为什么与其他添加十的方法相比,它不是一个无用和琐碎的观察。 - whitneyland
5
原始问题是“它是什么”,而不是为什么它有用。 - Adzz
4
咖喱化是将一个固定参数应用于现有函数的一种方式,以创建一个新的可重复使用的函数,而无需重新创建原始函数。这个答案很好地说明了这一点。 - tobius
4
我们至少可以做两件事:1. 缓存昂贵的操作 2. 在函数式编程范式中实现抽象。这是其他回答缺乏的“为什么有用”的解释。我认为这个回答也很好地解释了“什么”问题。 - drkvogel
显示剩余2条评论

50

柯里化是一种可以应用于函数的转换技术,使它们比以前少接收一个参数。

例如,在 F# 中你可以这样定义一个函数:

let f x y z = x + y + z

这里的函数f接受参数x、y和z,并将它们相加,因此:

f 1 2 3

返回6。

从我们的定义中,因此我们可以为f定义curry函数:

let curry f = fun x -> f x

其中 'fun x -> f x' 是一个lambda函数,与C#中的x => f(x)等价。这个函数输入您希望进行柯里化的函数,并返回一个函数,它接受单个参数并将指定函数的第一个参数设置为输入参数。

使用我们之前的例子,我们可以获得f的柯里化版本:

let curryf = curry f

我们可以接着执行以下操作:-
let f1 = curryf 1

这给我们提供了一个函数f1,它相当于f1 y z = 1 + y + z。这意味着我们可以进行以下操作:

f1 2 3

该过程返回6。

这个过程经常被误解为“部分函数应用”,可以这样定义:

let papply f x = f x

尽管我们可以将其扩展到多个参数,例如:

let papply2 f x y = f x y
let papply3 f x y z = f x y z
etc.

一个部分应用程序将函数和参数带入并返回一个需要少一个或多个参数的函数,正如前面两个例子所示,在标准的F#函数定义中直接实现,因此我们可以这样达到前面的结果:
let f1 = f 1
f1 2 3

这将返回6的结果。

总之:

柯里化和偏函数应用的区别在于:

柯里化接受一个函数并提供一个新函数来接收单个参数,并使用该参数设置指定函数的第一个参数,然后返回该函数。 这使我们能够将具有多个参数的函数表示为一系列单参数函数。例如:

let f x y z = x + y + z
let curryf = curry f
let f1 = curryf 1
let f2 = curryf 2
f1 2 3
6
f2 1 3
6

部分函数应用更为直接 - 它接受一个函数和一个或多个参数,并返回一个设置了前n个参数为指定的n个参数的函数。例如:

let f x y z = x + y + z
let f1 = f 1
let f2 = f 2
f1 2 3
6
f2 1 3
6

那么在C#中的方法需要进行柯里化才能进行部分应用吗? - cdmckay
"这使我们能够将具有多个参数的函数表示为一系列单参数函数。" - 很好,这清楚地解释了一切。谢谢。 - Fuzzy Analysis

36

柯里化函数是指将接受多个参数的函数重新编写,使其只接受第一个参数,并返回一个接受第二个参数的函数。以此类推,这样可以部分应用具有多个参数的函数的初始参数。


7
这使得具有多个参数的函数可以部分应用其初始参数。这有什么好处? - acarlon
7
函数经常被重复调用,其中一个或多个参数相同。例如,如果您想要将函数 f 映射到列表的列表 xss 上,可以执行 map (map f) xss - J D
1
谢谢,那很有道理。我多读了一些资料,现在明白了。 - acarlon
5
我认为这个答案以简洁的方式正确地解释了这个概念。所谓“柯里化”是将多个参数的函数转换为一系列只接受一个参数并返回单个参数函数(或在最后一个函数中返回实际结果)的过程。这可以由语言自动完成,或者您可以在其他语言中调用curry()函数来生成柯里化版本。需要注意的是,使用参数调用柯里化函数并不是柯里化。柯里化已经发生了。 - MindJuice

15

柯里化是将一个N元函数转换为N个一元函数的过程。函数的arity是指它需要的参数数量。

以下是正式定义:

 curry(f) :: (a,b,c) -> f(a) -> f(b)-> f(c)

以下是一个实际的例子,很容易理解:

你去取钱,刷卡输入密码,选择取款金额,然后按“确定”提交请求。

以下是正常取款功能。

const withdraw=(cardInfo,pinNumber,request){
    // process it
       return request.amount
}

在这个实现中,函数期望我们一次性输入所有参数。我们要刷卡、输入密码并发出请求,然后函数会运行。如果其中任何一步出了问题,你只能在输入完所有参数后得知。使用柯里化函数,我们将创建更高阶、纯净且简单的函数。纯净函数将帮助我们轻松调试代码。

这是使用柯里化函数的ATM:

const withdraw=(cardInfo)=>(pinNumber)=>(request)=>request.amount

自动取款机(ATM)将银行卡作为输入并返回一个期望pinNumber的函数,此函数又返回一个接受请求对象的函数,在成功处理后,您将获得要求的金额。每一步,如果出现错误,您都可以轻松预测出问题所在。比如,假设您插入了银行卡并出现错误,那么您就知道问题可能与银行卡或者机器有关,而不是pin号码。或者如果您输入了pin号码但无法通过验证,就知道您输入了错误的pin号码。这样,您就可以轻松地调试错误。

此外,这里的每个函数都是可重复使用的,因此您可以在项目的不同部分使用相同的函数。


9

柯里化是将一个可调用的函数,例如f(a, b, c)转换成一系列可调用的函数,例如f(a)(b)(c)

也可以说,柯里化就是将接受多个参数的函数拆分成一系列只接受部分参数的函数。

从字面上看,柯里化是一种函数转换:将一种调用方式转换为另一种。在JavaScript中,我们通常创建一个包装器来保留原始函数。

柯里化并不会调用函数,它只是对函数进行转换。

让我们创建一个curry函数,它可以对接受两个参数的函数进行柯里化。换句话说,对于二元函数f(a, b)curry(f)会将其转换为f(a)(b)

function curry(f) { // curry(f) does the currying transform
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let carriedSum = curry(sum);

alert( carriedSum(1)(2) ); // 3

正如您所看到的,这个实现是一系列的封装。

  • curry(func) 的结果是一个包装器 function(a)
  • 当像 sum(1) 这样调用时,参数将保存在词法环境中,并返回一个新的包装器 function(b)
  • 然后,sum(1)(2) 最终调用提供 2 的 function(b),并将调用传递给原始多参数 sum。

7

这是一个Python的玩具示例:

>>> from functools import partial as curry

>>> # Original function taking three parameters:
>>> def display_quote(who, subject, quote):
        print who, 'said regarding', subject + ':'
        print '"' + quote + '"'


>>> display_quote("hoohoo", "functional languages",
           "I like Erlang, not sure yet about Haskell.")
hoohoo said regarding functional languages:
"I like Erlang, not sure yet about Haskell."

>>> # Let's curry the function to get another that always quotes Alex...
>>> am_quote = curry(display_quote, "Alex Martelli")

>>> am_quote("currying", "As usual, wikipedia has a nice summary...")
Alex Martelli said regarding currying:
"As usual, wikipedia has a nice summary..."

(只是使用加号连接字符串以避免对非Python程序员的干扰。)
编辑添加:
请参见http://docs.python.org/library/functools.html?highlight=partial#functools.partial, 该文档还展示了Python实现中针对partial对象和函数区别的方式。

1
我正在使用partial对一个参数进行柯里化,生成一个具有两个参数的函数。如果你愿意,你可以进一步地把am_quote柯里化,创建一个只引用Alex在特定主题上的函数。数学背景可能会集中在最终得到仅有一个参数的函数上,但我相信像这样修复任意数量的参数通常(从数学角度来说并不精确)被称为柯里化。 - Anon
在你的评论之后,我进行了搜索并找到了其他参考资料,包括在SO上,以回应我熟悉的不精确用法的许多实例中“柯里化”和“部分应用”的区别。例如,请参见:https://dev59.com/X3VC5IYBdhLWcg3wqzLV - Anon
@Anon:我刚刚重新阅读了你的例子,发现我错过了重点。display_quote() 的第一个参数是在柯里化后生成 am_quote() 时出现的,而不是在 am_quote() 中出现的,但该值被携带。如果我手动分解 display_quote(),我可以产生三个函数,这些函数可以组合起来得到与 display_quote() 相同的结果,而不固定任何参数的值。调用 am_quote = curry(display_quote, "Alex Martelli") 看起来将 display_quote() 的第一个参数转换为常量字符串。这是柯里化的一部分吗?还是柯里化方法的副产品? - user140327
我认为这正是术语问题的关键所在。像许多其他人一样,我使用这个术语的方式是将参数连接起来,这正是柯里化的全部意义所在。但是,通过你的问题,我学到了其他人会同意“柯里化方法的产物”的说法。很高兴知道这种用法的差异存在于外界。 - Anon
@Anon - 嗯,这对我来说是一种相当新的、仍然有些奇怪的范式,所以我肯定还没有准备好表达个人偏好!知道使用上存在差异有助于理解这件事情。 - user140327
显示剩余3条评论

6

柯里化是JavaScript的高阶函数之一。

柯里化是一个带有多个参数的函数,它被重写为仅接收第一个参数并返回一个函数。该函数接着使用其余的参数并返回值。

感到困惑了吗?

让我们看一个例子:

function add(a,b)
    {
        return a+b;
    }
add(5,6);

这类似于以下的柯里化函数:
function add(a)
    {
        return function(b){
            return a+b;
        }
    }
var curryAdd = add(5);
curryAdd(6);

那么这段代码是什么意思呢?

现在重新阅读一下定义,

柯里化是一个有多个参数的函数,重写后它将获取第一个参数并返回一个函数,该函数会使用剩余的参数并返回值。

还是感到困惑吗? 让我深入解释一下!

当您调用此函数时,

var curryAdd = add(5);

它会返回一个像这样的函数:
curryAdd=function(y){return 5+y;}

因此,这被称为高阶函数。意思是,依次调用一个函数返回另一个函数是高阶函数的精确定义。这是JavaScript传奇所具有的最大优势。

回到柯里化:

这行代码将第二个参数传递给curryAdd函数。

curryAdd(6);

这反过来导致,

curryAdd=function(6){return 5+6;}
// Which results in 11

希望您理解这里柯里化的用法。那么,谈及优点,
为什么要使用柯里化?
它利用代码重复使用。 代码越少,错误越少。 你可能会问它如何减少代码?
我可以通过ECMA script 6新功能箭头函数来证明它。
是的!ECMA 6为我们提供了一个名为箭头函数的精彩特性。
function add(a)
    {
        return function(b){
            return a+b;
        }
    }

通过箭头函数的帮助,我们可以将上面的函数写成以下形式:

x=>y=>x+y

很酷,对吧?

因此,更少的代码和更少的错误!

借助这些高阶函数,可以轻松开发无bug的代码。

我向你挑战!

希望你理解了什么是柯里化。如果需要任何澄清,请随时在这里评论。

谢谢,祝你有美好的一天!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接