柯里化
柯里化
将多参数函数分为较小的一个参数函数
在一些基本类型的题外话之后,我们再次回到函数上,特别是我们前面提到的难题,如果数学函数只能有一个参数,那么F#的函数怎么能有多个参数?
答案很简单:多参数的函数被重写成一系列的单个参数的新函数。这是编译器为你自动完成的。一个在函数式编程中有重要影响的数学家,Haskell Curry之后,被称为"柯里化"。
我们用一个打印两个数字的基本实例,看看在实际中它是怎么运行的:
let printTwoParameters x y = printfn "x=%i y=%i" x y
本质上,编译器会重写成像这样 :
//explicitly curried version let printTwoParameters x = // only one parameter! let subFunction y = printfn "x=%i y=%i" x y // new function with one param subFunction // return the subfunction
我们仔细的检查一下
- 构建一个只有一个参数"x"的函数"printTwoParameters"。
- 内部,再构建一个只有一个参数"y"的子函数,注意内部的函数可以使用"x"而不需要传递它。"x"参数在作用域中,内部的函数可以知道和使用它,而不需要传递。
- 最后,返回创建的子函数。
- 返回的函数之后会用到"y"。参数"x"已经进入,所以返回的函数只需要参数"y"就可以完成函数逻辑。
通过重写的方式,必要的话,编译器保证函数只能有一个参数。所以当你使用"printTwoParameters"的时候,你可能想你是在用一个有两个参数的函数,但实际上只是一个单参数函数。你可通过只传一个参数而不是两个的方式自己看看:
// eval with one argument printTwoParameters 1 // get back a function! val it : (int -> unit) = <fun:printTwoParameters@286-3>
如果你只用一个参数去计算这个函数,不会编译错误,你会得到返回是一个函数。
当你用两个参数去调用"printTwoParameters"的时候,你真正在做的是:
- 你用第一个参数(x)调用printTwoParameters
- "x"进入函数,printTwoParameters返回一个新函数
- 你用第二个参数(y)调用新函数
下边这个例子是一步步演化的版本,和之后一个正常的版本:
// step by step version let x = 6 let y = 99 let intermediateFn = printTwoParameters x // return fn with // x "baked in" let result = intermediateFn y // inline version of above let result = (printTwoParameters x) y // normal version let result = printTwoParameters x y
这是另外一个例子:
//normal version let addTwoParameters x y = x + y //explicitly curried version let addTwoParameters x = // only one parameter! let subFunction y = x + y // new function with one param subFunction // return the subfunction // now use it step by step let x = 6 let y = 99 let intermediateFn = addTwoParameters x // return fn with // x "baked in" let result = intermediateFn y // normal version let result = addTwoParameters x y
重申一下,"两个参数函数" 实际上是一个返回中间函数的单参数函数。
但是等一下,"+"操作它自己怎么样?它是一个有两个参数的二进制运算,你确定?不是,它像是把每个函数都连起来了。有一个用一个参数返回的一个中间参数叫做"+"的函数,特别是像"addTwoParameters"。
当我们写"x+y"的声明时,编译器用一个有两个参数叫做+的函数移除中缀重新排序,使它成为"(+)x y"。注意叫做"+"的函数必须有一个全括号包括,表明它可以被当作正常函数名而不是中缀。
最后,有两个参数命名为+的函数将被视为其他有两个参数的函数。
// using plus as a single value function let x = 6 let y = 99 let intermediateFn = (+) x // return add with x baked in let result = intermediateFn y // using plus as a function with two parameters let result = (+) x y // normal version of plus as infix operator let result = x + y
而且,它对其他操作符也有效,像printf一样构建函数:
// normal version of multiply let result = 3 * 5 // multiply as a one parameter function let intermediateFn = (*) 3 // return multiply with "3" baked in let result = intermediateFn 5 // normal version of printfn let result = printfn "x=%i y=%i" 3 5 // printfn as a one parameter function let intermediateFn = printfn "x=%i y=%i" 3 // "3" is baked in let result = intermediateFn 5
柯里化函数签名
现在我们知道柯里化函数如何运行,我们期待它的函数签名是什么样的呢?
回到第一个例子,"printTwoParameters",我们看到接受一个参数,返回一个中间函数。中间函数也是接受一个参数什么也不返回(unit)。所以中间函数是这样的签名int->unit。换句话说,printTwoParameters的域是int,它的范围是int->unit。把它们放在一起我们看到最终的签名是:
val printTwoParameters : int -> (int -> unit)
如果你计算显式curry过的实现,你会在签名中看到括号,像上边写的,但是如果你计算隐式curry过的实现,括号就不见了,像这样:
val printTwoParameters : int -> int -> unit
括号是可选的。如果你想试着使函数签名便于理解,在心里加上它们是有帮助的。
你可能在想这点,返回一个中间函数的函数和一个两个参数的常规函数有什么不同?
这是一个接受一个参数返回一个函数的函数:
let add1Param x = (+) x // signature is = int -> (int -> int)
这是一个接受两个参数返回简单值的函数:
let add2Params x y = (+) x y // signature is = int -> int -> int
它们的签名轻微的不同,具体来说,它们没有不同。只是第二个函数是为你自动curry过的。
超过两个参数的函数
如果超过两个参数柯里化是什么运行的?完全相同的方式:除最后一个函数外,函数返回一个上一个参数进入作用域的中间函数。
思考这个人为的例子。我已经显示的指出参数的类型,但是函数本身什么也不做。
let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () //do nothing let intermediateFn1 = multiParamFn 42 // intermediateFn1 takes a bool // and returns a new function (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2 takes a string // and returns a new function (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 takes a float // and returns a simple value (unit) let finalResult = intermediateFn3 3.141
综合的函数的签名是:
val multiParamFn : int -> bool -> string -> float -> unit
中间函数的签名是:
val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = ()
函数的签名可以告诉你函数接受的参数个数,计算括号外边箭头的数量。如果函数接受或者返回其他函数参数,括号里会有其他箭头,但是这些都是被忽略的。看看这些例子:
int->int->int // two int parameters and returns an int string->bool->int // first param is a string, second is a bool, // returns an int int->string->bool->unit // three params (int,string,bool) // returns nothing (unit) (int->string)->int // has only one parameter, a function // value (from int to string) // and returns a int (int->string)->(int->bool) // takes a function (int to string) // returns a function (int to bool)
多参数的问题
如果你不理解柯里化背后的逻辑,它会产生一些意想不到的结果。记住,如果你计算一个比期望参数数量少的参数时,你不会出错。相反,你会得到一个部分应用过的函数,如果你在一个期望值的上下文中继续使用这个函数,编译器会给出一个模糊的错误信息。
这是一个无害的函数:
// create a function let printHello() = printfn "hello"
调用的时候,我们期望会发生什么?它会在控制台打印"hello"吗?计算之前猜一下,有一个提示:一定要看一下函数的签名。
// call it printHello
他不会像期望的被调用。原函数期望一个unit参数没有被提供,所以你得到一个部分应用过的函数(这里指没有参数)。
这个怎么样?它会被编译吗?
let addXY x y = printfn "x=%i y=%i" x x + y
如果你计算这个函数,你会看到编译器在printf这一行上抱怨:
printfn "x=%i y=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, i.e. is missing //arguments. Its type is ^a -> unit.
如果你不理解柯里化,这个信息是很神秘的。独立计算的所有的表达式像(也就是,没有用作返回值或者用"let"绑定点东西)必须计算unit值。在这个例子中,它没有计算unit的值,而不是没有计算函数。说那么多就是说printf少一个参数。
用.NET库交互的时候像这样是一个常见的错误。例如"TextReader"的"ReadLine"方法必须接受一个unit参数。很容易忘记这些和括号,这种情况下,你不会理解得到一个编译错误,只有当你把结果作为字符串处理时。
let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // wrong but compiler doesn't // complain printfn "The line is %s" line1 //compiler error here! // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() //correct printfn "The line is %s" line2 //no compiler error
在上边的例子中,line1只是一个指针或者是委托给ReadLine方法,而不是我们期望的字符串。ReadLine()中()的用法才是执行函数。
太多参数
当你有很多参数时,你也会得到一个神秘的信息。这是一些给printf传递很多参数的例子:
printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit
例如,在最后一个例子中,编译器说它期望格式参数需要三个参数('a->'b->'c->'d有三个参数),但是只给了它两个(签名'a->'b->unit有两个参数)。
在不使用printf的情况下,传递很多参数意味着,你以一个简单值结束之后还要传递一个参数。编译器就会抱怨简单值不是函数。
let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied
如果你打破了一系列显式的中间函数的调用,像我们之前做的,你可以明确的看到错误。
let add1 x = x + 1 let intermediateFn = add1 2 //returns a simple value let x = intermediateFn 3 //intermediateFn is not a function! // ==> error FS0003: This value is not a function // and cannot be applied
翻译有误,请指正,谢谢!
原文地址:http://fsharpforfunandprofit.com/posts/currying/
翻译目录传送门:http://www.cnblogs.com/JayWist/p/5837982.html