什么是“咖喱”?

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

216608 次浏览

柯里化是将一个接受多个参数的函数分解为一系列函数,每个函数只接受一个参数。这是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。

柯里化是一种可以应用于函数的转换,允许它们比以前少一个参数。

例如,在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

其中'funx->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 ylet papply3 f x y z = f x y zetc.

部分应用程序将获取函数和参数并返回一个需要一个或多个较少参数的函数,正如前面两个示例所示,它直接在标准F#函数定义中实现,因此我们可以实现前面的结果:-

let f1 = f 1f1 2 3

这将返回6的结果。

总结:-

柯里化和部分函数应用程序之间的区别在于:-

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

let f x y z = x + y + zlet curryf = curry flet f1 = curryf 1let f2 = curryf 2f1 2 36f2 1 36

偏函数应用程序更直接-它接受一个函数和一个或多个参数,并返回一个函数,其中前n个参数设置为指定的n个参数。示例:-

let f x y z = x + y + zlet f1 = f 1let f2 = f 2f1 2 36f2 1 36

柯里化函数是一个由多个参数组成的函数,它重写后接受第一个参数并返回一个接受第二个参数的函数,依此类推。这允许多个参数的函数部分应用它们的一些初始参数。

在函数代数中,处理具有多个参数(或等效的一个参数是N元组)的函数有点不优雅-但是,正如Moses Schönfinkel(以及独立的Haskell Curry)所证明的那样,它是不需要的:您所需要的只是一个参数的函数。

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

像往常一样,wikipedia有一个很好的总结条目,有许多有用的指针(可能包括关于你最喜欢的语言的指针;-)以及稍微更严格的数学处理。

我发现这篇文章和它引用的文章很有用,可以更好地理解柯里化:http://blogs.msdn.com/wesdyer/archive/2007/01/29/currying-and-partial-function-application.aspx

正如其他人提到的,它只是一种具有单参数函数的方法。

这很有用,因为您不必假设将传入多少参数,因此您不需要2个参数、3个参数和4个参数函数。

这里有一个具体的例子:

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

现在,在地球上,你只想计算地球上物体的引力。用函数式语言,你可以将地球的质量传递给函数,然后部分评估它。你得到的是另一个函数,它只需要两个参数,就可以计算地球上物体的引力。这叫做柯里化。

这是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实现它的方式中的部分对象与函数的区别。

柯里化函数应用于多个参数列表,而不仅仅是一。

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

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

这是类似的柯里化函数。相反对于一个包含两个Int参数的列表,您将此函数应用于两个包含一个Int参数的列表int参数each:

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

这里发生的事情是,当你调用curriedSum时,你实际上会得到两个背靠背的传统函数调用。第一个函数调用接受一个名为x的Int参数,并返回一个函数第二个函数的值。第二个函数接受Int参数y.

这是一个名为first的函数,它在精神上做了第一个传统的curriedSum的函数调用会做:

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

将1应用于第一个函数-换句话说,调用第一个函数然后传入1-产生第二个函数:

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

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

scala> second(2)res6: Int = 3

如果你理解了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需要两个参数。当我们传递给它一个参数时,而不是犹豫,如果函数部分应用了我们代表我们传递的参数,理解我们可能打算稍后提供另一个参数,会怎么样?然后我们可以在不明确使用partial的情况下定义inc

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

这是一些语言的行为方式。当人们希望将函数组合成更大的转换时,它非常有用。这将导致一个转换器。

它可以是一种使用函数来制作其他函数的方法。

在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()总是给我们传入的任何东西加上10。我们可以用同样的方式制作类似的函数:

let addTwo = add(2)       // addTwo(); will add two to whatever you pass inlet 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)

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

柯里化的一个例子是,当有函数时,您目前只知道其中一个参数:

例如:

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/闭包才能将其发送到函数。

正如所有其他答案一样,柯里化有助于创建部分应用的函数。Javascript不提供对自动柯里化的原生支持。因此,上面提供的示例可能对实际编码没有帮助。livescript中有一些很好的示例(本质上编译为js)http://livescript.net/

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

在上面的例子中,当你给出更少的参数时,livescript会为你生成新的柯里化函数(双)

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

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

这样做的好处是可以一次处理一个参数,从而简化参数的处理,同时也可以提高程序的灵活性和易读性,这也使得程序更易于管理,并且将代码分成更小的片段也会使其易于重用。

例如:

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

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

在这里你可以找到C#中柯里化实现的简单解释。在评论中,我试图展示柯里化是如何有用的:

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

柯里化是将一个函数从可调用的f(a, b, c)转换为可调用的f(a)(b)(c)

否则,柯里化是指将一个接受多个参数的函数分解为一系列接受部分参数的函数。

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

柯里化不调用函数。它只是转换它。

让我们创建一个为双参数函数执行柯里化的curry函数。换句话说,curry(f)对于双参数f(a, b)将其转换为f(a)(b)

function curry(f) { // curry(f) does the currying transformreturn function(a) {return function(b) {return f(a, b);};};}
// usagefunction 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)最后调用function(b)提供2,并将调用传递给原始的多参数和。

这是一个泛型的例子,也是函数柯里化的最短版本,有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)());

有一个“在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));};

柯里化是Java脚本的高阶函数之一。

柯里化是一个由许多参数组成的函数,它被重写为接受第一个参数并返回一个函数,该函数依次使用剩余的参数并返回值。

困惑?

让我们看一个例子,

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

所以,这称为高阶函数。意思是,依次调用一个函数返回另一个函数是高阶函数的精确定义。这是图例,Java脚本的最大优势。让我们来谈谈这个问题

这一行将把第二个参数传递给curryAdd函数。

curryAdd(6);

这反过来导致,

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

希望你能理解这里的咖喱用法。所以,在优势方面,

为什么咖喱?

它利用代码的可重用性。更少的代码,更少的错误。你可能会问它是如何减少代码的?

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

是的!ECMA 6,为我们提供了称为箭头函数的奇妙功能,

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

借助箭头函数,我们可以将上面的函数编写如下,

x=>y=>x+y

很酷吧?

所以,更少的代码和更少的错误!!

在这些高阶函数的帮助下,可以很容易地开发出bug的代码。

我挑战你!

Hope,你已经理解了什么是咖喱。如果你需要解释什么,请在这里发表评论。

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

“柯里化”是将多个参数的函数转换为一系列函数的过程,每个函数都接受一个参数并返回一个参数的函数,或者在最终函数的情况下,返回实际结果。

柯里化是指将一个N度的函数转换为N个1度的函数。函数的arity是它需要的参数数。

以下是正式定义:

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

这是一个真实世界的例子,很有意义:

你去自动取款机取钱。你刷卡,输入密码,进行选择,然后按回车键在请求旁边提交“金额”。

这里是正常的取款功能。

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

在这个实现中,函数期望我们一次输入所有参数。我们要刷卡,输入pin并发出请求,然后函数就会运行。如果这些步骤中的任何一个有问题,你输入所有参数后就会发现。使用柯里化函数,我们将创建更高精度、纯粹和简单的函数。纯函数将帮助我们轻松调试代码。

这是具有柯里化功能的Atm:

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

ATM,将卡作为输入并返回一个需要pin Number的函数,该函数返回一个接受请求对象的函数,并在成功处理后,您获得您请求的金额。每一步,如果您有错误,您将很容易预测出哪里出了问题。假设您输入卡并得到错误,您知道它与卡或机器有关,但与引脚号无关。或者如果您输入了引脚,如果它没有被接受,您知道您输入的引脚号错误。您将很容易调试错误。

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

下面是JavaScript中的一个柯里化示例,这里的乘以返回用于将x乘以2的函数。

const multiply = (presetConstant) => {return (x) => {return presetConstant * x;};};
const multiplyByTwo = multiply(2);
// now multiplyByTwo is like below function & due to closure property in JavaScript it will always be able to access 'presetConstant' value// const multiplyByTwo = (x) => {//   return presetConstant * x;// };
console.log(`multiplyByTwo(8) : ${multiplyByTwo(8)}`);

产出

//乘法二(8): 16

其他答案已经说明了柯里化是什么:向柯里化函数传递比预期更少的参数不是错误,而是返回一个函数,该函数期望其余参数并返回相同的结果,就好像你一次传递了所有参数一样。

我将试着解释它为什么有用。它是那些你从未意识到你需要的工具之一,直到你这样做。柯里化首先是一种让你的程序更具表现力的方法——你可以用更少的代码将操作组合在一起。

例如,如果您有一个柯里化函数add,您可以将JSx => k + x(或Python lambda x: k + x或Ruby{ |x| k + x }或Lisp(lambda (x) (+ k x))或…)的等价物编写为仅add(k)。在Haskelll中,您甚至可以使用运算符:(k +)(+ k)(这两种形式允许您对非交换运算符进行任何一种柯里化:(/ 9)是一个将数字除以9的函数,这可能是更常见的用例,但您也可以将(9 /)用于将9除以其参数的函数。)除了更短之外,柯里化版本不包含像所有其他版本中的x => k + x0那样的虚构参数名称。它不需要。你正在定义一个函数,它向一个数字添加一些常量k,你不需要为该数字命名只是为了谈论这个函数。甚至不需要定义它。这是所谓的“点自由风格”的一个例子。你可以将操作组合在一起,只给出操作本身。你不必声明什么都不做的匿名函数,只是对它们的参数应用一些操作,因为*这就是操作已经是的。

当高阶函数以柯里化友好的方式定义时,这变得非常方便。例如,柯里化的map(fn, list)让你定义一个只有map(fn)的映射器,稍后可以将其应用于任何列表。但是柯里化定义为map(list, fn)的映射只会让你定义一个将其他函数应用于常量列表的函数,这可能不太有用。

柯里化减少了对管道和线程等东西的需求。在Clojure中,你可以使用线程宏->(defn f2c (deg) (-> deg (- 32) (* 5) (/ 9))定义一个温度转换函数。这很酷,它从左到右读起来很好(“减去32,乘以5,除以9。”)而且你只需在每个子操作中提到参数两次而不是一次……但它之所以有效,是因为->是一个宏,在计算任何东西之前都会在语法上转换整个形式。它在幕后变成了一个正则嵌套表达式:(/ (* (- deg 32) 5) 9)。如果数学运算被柯里化,你就不需要宏来很好地组合它们,就像在Haskelllet f2c = (subtract 32) & (* 5) & (/ 9)中一样。(尽管使用函数组合会更惯用,从右到左读取:(/ 9) . (* 5) . (subtract 32)

再一次,很难找到好的演示示例;咖喱在复杂的情况下是最有用的,它确实有助于解决方案的易读性,但是那些需要太多的解释才能让你理解这个问题,以至于关于咖喱的整体教训可能会迷失在噪音中。

这个线程中的大多数示例都是人为的(添加数字)。这些对于说明概念很有用,但是当你可能在应用程序中实际使用柯里化时,不要激励你。

这是JavaScript用户交互界面库React的一个实际示例。这里的柯里说明了闭包属性。

在大多数用户交互界面库中,当用户单击按钮时,会调用一个函数来处理事件。处理程序通常会修改应用程序的状态并触发界面重新呈现。

项目列表是常见的用户交互界面组件。每个项目可能有一个与之关联的标识符(通常与数据库记录相关)。当用户单击按钮时,例如,“喜欢”列表中的项目,处理程序需要知道单击了哪个按钮。

柯里化是实现id和处理程序之间绑定的一种方法。在下面的代码中,makeClickHandler是一个接受id并返回在其范围内具有id的处理程序函数的函数。

内部函数的工作原理在本讨论中并不重要。但是如果你好奇,它会搜索项目数组以按id查找一个项目,并增加其“喜欢”,通过设置状态触发另一个渲染。State在React中是不可变的,所以修改一个值需要比你预期的更多的工作。

你可以将调用柯里化函数视为“剥离”外部函数以公开准备调用的内部函数。那个新的内部函数是传递给ReactonClick的实际处理程序。外部函数用于循环主体指定将在特定内部处理程序函数范围内的id。

const List = () => {const [items, setItems] = React.useState([{name: "foo", likes: 0},{name: "bar", likes: 0},{name: "baz", likes: 0},].map(e => ({...e, id: crypto.randomUUID()})));
//    .----------.   outer func  inner func//    | currying |         |       |//    `----------`         V       Vconst makeClickHandler = (id) => (event) => {setItems(prev => {const i = prev.findIndex(e => e.id === id);const cpy = {...prev[i]};cpy.likes++;return [...prev.slice(0, i),cpy,...prev.slice(i + 1)];});};
return (<ul>{items.map(({name, likes, id}) =><li key={id}><buttononClick={/* strip off first function layer to get a clickhandler  bound to `id` and pass it to onClick */makeClickHandler(id)}>{name} ({likes} likes)</button></li>)}</ul>);};
ReactDOM.render(<List />,document.querySelector("#root"));
button {font-family: monospace;font-size: 2em;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script><div id="root"></div>