什么是“咖喱”?


652

我在几篇文章和博客中都看到过关于咖喱函数的引用,但我找不到很好的解释(或者至少有一个合理的解释!)


12
[作为注释,因为它对非数学家来说是无用的。]根据笛卡尔封闭类别的定义,在X-> X x A和X之间存在固定的附加族(由A参数化)。 -> X ^ A.同构hom(X x A,Y)<-> hom(X,Y ^ A)是Haskell 的curryuncurry函数。这里重要的是这些同构是预先固定的,因此是语言的“内置”。
Alexandre C.

3
有一个很好的教程这里在Haskell讨好learnyouahaskell.com/higher-order-functions#curried-functions短的注释是,add x y = x+y(咖喱)是不同的add (x, y)=x+y(uncurried)
Jaider

Answers:


871

咖喱化是指将一个包含多个参数的函数分解为一系列仅包含一个参数的函数。这是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);

var add3 = add(3);

add3(4);

第一条语句返回7,就像add(3,4)语句一样。第二条语句定义了一个名为add3的新函数,该函数会将3加到其参数中。这就是某些人所谓的关闭。第三条语句使用add3操作将3加到4,结果再次产生7。


235
从实际意义上讲,我该如何利用这个概念?
草莓

43
@Strawberry,例如,假设您有一个数字列表,[1, 2, 3, 4, 5]希望将其乘以任意数字。在Haskell中,我可以编写map (* 5) [1, 2, 3, 4, 5]将整个列表乘以5,从而生成list [5, 10, 15, 20, 25]
nyson

62
我了解map函数的作用,但是我不确定是否了解您要为我说明的观点。您是在说map函数代表currying概念吗?
草莓

78
@Strawberry的第一个参数map必须是仅接受一个参数的函数-列表中的元素。作为数学概念,乘法是一种二进制运算。它需要2个参数。但是,在Haskell中*是咖喱函数,类似于add此答案的第二个版本。结果(* 5)是一个函数,该函数接受单个参数并将其乘以5,并允许我们将其与map一起使用。
2014年

26
@Strawberry关于诸如标准ML或Haskell之类的功能语言的好处是,您可以“免费”获取信息。您可以像使用其他任何语言一样定义多参数函数,并且自动获得该函数的咖喱版本,而不必自己添加一堆lambda。因此,您可以生成新函数,这些函数从任何现有函数中获取较少的参数而不会引起大惊小怪或麻烦,并且可以轻松地将它们传递给其他函数。
2014年

125

在函数的代数中,处理带有多个参数(或等效的一个参数为N元组)的函数有些微不足道-但是,正如MosesSchönfinkel(以及独立的Haskell Curry)证明的那样,不需要它:所有人需要的是带有一个参数的函数。

那么,您如何处理自然表达的东西f(x,y)呢?好吧,您将其等同于f(x)(y)- f(x),称为它g,是一个函数,然后将该函数应用于y。换句话说,您只有带有一个参数的函数-但是其中一些函数会返回其他函数(也带有一个参数;-)。

像往常一样,维基百科对此有一个很好的总结条目,其中包含许多有用的指针(可能包括与您喜欢的语言有关的指针;-)以及更为严格的数学处理方法。


1
我想上面我也有类似的评论-我还没有看到函数式语言将函数限制为采用单个arg。我错了吗?
Eric M

1
@hoohoo:函数式语言通常不会将函数限制为单个参数。但是,在较低的数学水平上,处理仅带有一个参数的函数要容易得多。(例如,在lambda演算中,函数一次仅接受一个参数。)
Sam DeFabbia-Kane,2009年

1
好。另一个问题。以下是正确的陈述吗?Lambda演算可以用作函数编程的模型,但是函数编程不一定要应用Lambda演算。
Eric M

7
正如维基百科页面所指出的那样,大多数FP语言都“赞扬”或“增强”了lambda演算(例如,具有一些常量和数据类型),而不仅仅是“应用”了它,但还不是很接近。顺便说一句,您给人的印象是,例如Haskell不会“将函数限制为采用单个arg”吗?当然可以,尽管这与curry无关。例如div :: Integral a => a -> a -> a-注意那些多个箭头?“将a映射为功能,将a映射为a”是一个读法;-)。您可以div&c 使用(单个)元组参数,但这在Haskell中确实是反习惯用法的。
Alex Martelli,2009年

@Alex-wrt ​​Haskell&arg伯爵,我花了很多时间在Haskell上,而这一切都是在几周前完成的。因此,这是一个容易犯的错误。
Eric M

99

这是一个具体的例子:

假设您有一个计算作用在物体上的重力的函数。如果您不知道公式,可以在这里找到。此函数将三个必需参数作为参数。

现在,在地球上,您只想计算该星球上物体的力。用功能语言,您可以将地球质量传递给功能,然后对其进行部分评估。您会得到的是另一个仅包含两个参数并计算地球上物体的重力的函数。这被称为咖喱。


2
出于好奇,JavaScript的Prototype库提供了一个“ curry”函数,其功能几乎与您在此处说明的完全一样:prototypejs.org/api/function/curry
shuckster,2009年


7
这听起来像是我的部分应用。我的理解是,如果应用currying,则可以使用单个参数创建函数并将其组合以形成更复杂的函数。我想念什么吗?
neontapir

9
@neontapir是正确的。乳木果描述的不是咖喱。它是部分应用程序。如果使用三参数函数,并且将其称为f(1),则返回的不是二参数函数。您返回一个单参数函数,该函数返回另一个单参数函数。咖喱函数只能传递一个参数。PrototypeJS中的curry函数也不是唯一的。这是部分应用程序。
MindJuice

否(进行部分评估)和否(进行评估)。这称为部分应用程序。要启用该功能,需要进行货币兑换。
内斯

47

Currying是一种可以应用于函数的转换,以使它们比以前少使用一个参数。

例如,在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-> fx'是Lambda函数,等效于C#中的x => f(x)。该函数输入您要咖喱的函数,并返回一个带有单个参数的函数,并返回将第一个参数设置为输入参数的指定函数。

使用前面的示例,我们可以得到f的咖喱:-

let curryf = curry f

然后,我们可以执行以下操作:

let f1 = curryf 1

它为我们提供了一个函数f1,它等于f1 yz = 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。

结论:-

currying和部分函数应用程序之间的区别在于:

Currying具有一个函数,并提供一个接受单个参数的新函数,并返回其第一个参数设置为该参数的指定函数。这使我们可以将具有多个参数的函数表示为一系列单参数函数。例:-

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 2012年

“这使我们可以将具有多个参数的函数表示为一系列单参数函数”-完美,对我来说一切都很好。谢谢
模糊分析

44

它可以是使用功能来实现其他功能的一种方式。

在javascript中:

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

将允许我们这样称呼它:

let addTen = add(10);

在运行时,将10作为传入x

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)

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


5
我在这里看到的关于固有顺序过程的最佳逐步说明,也许是最好的,最具解释性的答案。

4
@jonsilver我会说相反的话,不是一个很好的解释。我同意这样做可以很好地说明所举的例子,但是人们倾向于不去思考,“是的,很清楚,但是我本可以用另一种方式做同样的事情,那么到底有什么好处呢?” 换句话说,我希望它有足够的上下文或解释,以阐明照明的工作原理,以及与其他加十的方法相比,为什么它不是无用且琐碎的观察。
whitneyland '18

29

curried函数是重写了几个参数的函数,这样它可以接受第一个参数,并返回一个接受第二个参数的函数,依此类推。这允许几个自变量的功能部分地应用其某些初始自变量。


5
“这允许几个参数的函数部分地应用其某些初始参数。” -为什么有好处?
acarlon 2013年

5
@acarlon函数通常在一个或多个参数相同的情况下被重复调用。例如,如果要在列表列表中使用map某个函数,则可以执行。fxssmap (map f) xss
乔恩·哈罗普

1
谢谢,这很有意义。我做了更多的阅读,它已经到位。
acarlon

4
我认为这个答案以一种简洁的方式正确了。“ currying”是采用多个参数的功能并将其转换为严肃的功能的过程,每个功能都采用一个参数并返回一个参数的功能,或者对于最终功能,则返回实际结果。可以通过语言自动为您完成此操作,也可以用其他语言调用curry()函数来生成咖喱版本。请注意,使用参数调用咖喱函数并不容易。欺骗已经发生了。
MindJuice

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实现此方法的部分对象与函数的区别。


我不明白这一点-您可以这样做:>>> am_quote = curry(display_quote,“ Alex Martelli”),但是接下来您可以这样做:>>> am_quote(“ currying”,“和往常一样,维基百科的摘要很不错。 ..“),所以您有一个带有两个参数的函数。似乎curring应该给您提供您将要编写的三个不同的func?
Eric M

我正在使用partial来仅咖喱一个参数,产生一个带有两个参数的函数。如果需要,可以进一步咖喱am_quote来创建一个仅引用特定主题的Alex。数学背景可能专注于最终仅具有一个参数的函数-但我认为固定这样的任意数量的参数通常称为currying(如果从数学的角度来看不精确)。
佚名

(顺便说一句,“ >>>”是Python交互式解释器中的提示,而不是代码的一部分。)
Anon,2009年

好,谢谢您对args的澄清。我知道Python解释器的提示,我试图用引号括起来,但没有用;-)
Eric M

在您发表评论之后,我搜索并找到了其他参考,包括“ currying”和“ currying”之间的区别。针对部分我不熟悉的用法的“部分应用程序”。参见例如:stackoverflow.com/questions/218025/…– 2009
匿名

5

咖喱化是将函数从可调用f(a, b, c)转换为可调用f(a)(b)(c)

否则,当您分解一个将多个参数合并为一部分参数的函数时,就会遇到麻烦。

从字面上看,currying是功能的转换:从一种调用方式转换为另一种调用方式。在JavaScript中,我们通常做一个包装来保留原始功能。

咖喱不会调用函数。它只是改变了它。

让我们使咖喱函数对两个参数的函数执行curring。换句话说,curry(f)因为两个参数f(a, b)将其转换为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)
  • 当调用like时sum(1),参数将保存在Lexical Environment中,并返回一个新的包装器function(b)
  • 然后sum(1)(2)最终调用function(b)提供2,并将调用传递给原始的多参数和。

4

如果您知道partial您已经走了一半。的想法partial是将参数预先应用到函数,并返回仅需要其余参数的新函数。调用此新函数时,它将包括预加载的参数以及提供给它的任何参数。

Clojure中+有一个函数,但要使事情变得清晰起来:

(defn add [a b] (+ a b))

您可能会意识到,该inc函数只是将传递的数字加1。

(inc 7) # => 8

让我们使用partial以下命令自己构建它:

(def inc (partial add 1))

在这里,我们返回另一个函数,该函数的的第一个参数加载了1 add。如add需要两个参数一样,新inc函数只需要b参数-而不是像以前那样使用2个参数,因为已经部分应用了1个。因此,这partial是一个用于创建具有默认值的新功能的工具。这就是为什么在函数式语言中,函数通常将参数从通用到特定进行排序。这使得重用此类功能以构造其他功能更加容易。

现在,假设该语言是否足够聪明,可以内省地理解 add需要两个参数。当我们传递一个参数而不是阻止它时,如果函数部分地应用了该参数,那么我们代表我们传递了它,而又意识到我们可能打算稍后再提供另一个参数?然后,我们可以在inc不显式使用的情况下进行定义partial

(def inc (add 1)) #partial is implied

这是某些语言的行为方式。当人们希望将功能组合成更大的转换时,它特别有用。这将导致换能器。



3

正如所有其他答案一样,currying有助于创建部分应用的功能。Javascript不提供对自动执行的本机支持。因此,上面提供的示例可能对实际编码没有帮助。livescript中有一些出色的示例(实质上是编译为js) http://livescript.net/

times = (x, y) --> x * y
times 2, 3       #=> 6 (normal use works as expected)
double = times 2
double 5         #=> 10

在上面的示例中,当您没有给出任何参数时,livescript为您生成新的咖喱函数(双精度)


3

Curry可以简化您的代码。这是使用此功能的主要原因之一。咖喱化是将接受n个参数的函数转换为仅接受一个参数的n个函数的过程。

原理是使用闭包(closure)属性传递传递的函数的参数,将其存储在另一个函数中并将其视为返回值,这些函数形成一个链,最后的参数传递给完成操作。

这样做的好处是它可以通过一次处理一个参数来简化参数的处理,这也可以提高程序的灵活性和可读性。这也使程序更易于管理。同样,将代码分成较小的部分将使其易于重用。

例如:

function curryMinus(x) 
{
  return function(y) 
  {
    return x - y;
  }
}

var minus5 = curryMinus(1);
minus5(3);
minus5(5);

我也可以

var minus7 = curryMinus(7);
minus7(3);
minus7(5);

这对于使复杂的代码整齐并处理不同步的方法等非常有用。


2

curried函数应用于多个参数列表,而不只是一个。

这是一个常规的非咖喱函数,它添加了两个Int参数x和y:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int,y: Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3

这是类似的函数。将此函数而不是两个Int参数的一个列表,而是将此函数应用于每个一个Int参数的两个列表:

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (x: Int)(y: Int)Intscala> second(2)
res6: Int = 3
scala> curriedSum(1)(2)
res5: Int = 3

这里发生的是,当您调用时curriedSum,您实际上背对背获得了两个传统的函数调用。第一个函数调用采用一个名为的Int参数x,并返回第二个函数的函数值。第二个函数采用Int参数 y

这是一个名为的函数first,它实际上curriedSum会执行第一个传统函数调用将执行的操作:

scala> def first(x: Int) = (y: Int) => x + y
first: (x: Int)(Int) => Int

将1应用于第一个函数(换句话说,调用第一个函数并传入1)将产生第二个函数:

scala> val second = first(1)
second: (Int) => Int = <function1>

将2应用于第二个函数将产生结果:

scala> second(2)
res6: Int = 3

2

currying的一个示例是拥有函数时,您目前仅知道其中一个参数:

例如:

func aFunction(str: String) {
    let callback = callback(str) // signature now is `NSData -> ()`
    performAsyncRequest(callback)
}

func callback(str: String, data: NSData) {
    // Callback code
}

func performAsyncRequest(callback: NSData -> ()) {
    // Async code that will call callback with NSData as parameter
}

在这里,由于您不知道要向其发送回调的第二个参数,因此performAsyncRequest(_:)您必须创建另一个lambda /闭包才能将其发送给函数。


func callback返回自己?它被称为@ callback(str)so let callback = callback(str),回调只是func callback
nikk wong

不,func callback(_:data:)接受两个参数,这里我只给它一个,String所以它在等待下一个(NSData),这就是为什么现在let callback另一个函数在等待数据传递
S2dent

2

这是n为n的函数的通用和最短版本的示例。的参数。

const add = a => b => b ? add(a + b) : a; 

const add = a => b => b ? add(a + b) : a; 
console.log(add(1)(2)(3)(4)());


1

在这里,您可以找到有关C#中的currying实现的简单说明。在评论中,我试图说明如何使用curring:

public static class FuncExtensions {
    public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> func)
    {
        return x1 => x2 => func(x1, x2);
    }
}

//Usage
var add = new Func<int, int, int>((x, y) => x + y).Curry();
var func = add(1);

//Obtaining the next parameter here, calling later the func with next parameter.
//Or you can prepare some base calculations at the previous step and then
//use the result of those calculations when calling the func multiple times 
//with different input parameters.

int result = func(1);

1

咖喱是Java Script的高级功能之一。

Currying是许多参数的函数,这些参数被重写,使得它采用第一个参数并返回一个函数,该函数又使用剩余的参数并返回值。

困惑?

让我们看一个例子,

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

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

现在再次阅读定义,

Currying是许多参数的函数,这些参数被重写,以便它采用第一个参数并返回一个函数,该函数又使用剩余的参数并返回值。

仍然感到困惑吗?让我深入解释!

当您调用此函数时,

var curryAdd = add(5);

它将返回一个这样的函数,

curryAdd=function(y){return 5+y;}

因此,这称为高阶函数。意思是,依次调用一个函数会返回另一个函数,这是对高阶函数的精确定义。对于传奇Java脚本,这是最大的优势。因此,回到混乱的状态,

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

curryAdd(6);

反过来结果,

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

希望您在这里了解curring的用法。因此,利用优势,

为什么要咖喱?

它利用了代码的可重用性。更少的代码,更少的错误。您可能会问,它少了多少代码?

我可以使用ECMA脚本6个新功能箭头功能来证明这一点。

是! ECMA 6为我们提供了称为箭头功能的出色功能,

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

借助arrow函数,我们可以编写以下函数,

x=>y=>x+y

酷吧?

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

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

我挑战你!

希望,您了解正在发生的事情。如果您需要任何说明,请随时在这里发表评论。

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


0

有一个“在ReasonML中固化”的示例。

let run = () => {
    Js.log("Curryed function: ");
    let sum = (x, y) => x + y;
    Printf.printf("sum(2, 3) : %d\n", sum(2, 3));
    let per2 = sum(2);
    Printf.printf("per2(3) : %d\n", per2(3));
  };
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.