柯里化

柯里化

将多参数函数分为较小的一个参数函数


在一些基本类型的题外话之后,我们再次回到函数上,特别是我们前面提到的难题,如果数学函数只能有一个参数,那么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

我们仔细的检查一下

  1. 构建一个只有一个参数"x"的函数"printTwoParameters"。
  2. 内部,再构建一个只有一个参数"y"的子函数,注意内部的函数可以使用"x"而不需要传递它。"x"参数在作用域中,内部的函数可以知道和使用它,而不需要传递。
  3. 最后,返回创建的子函数。
  4. 返回的函数之后会用到"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

 

posted @ 2016-09-10 02:09  JayWist  阅读(784)  评论(0编辑  收藏  举报