简短介绍了基本类型之后,我们可以再次返回到函数。 特别是对于前面提到的谜语:如果一个数学函数只能采用一个参数,那么F#中怎么会有一个函数采用大量参数呢? 削减更多细节!

答案很简单:将具有多个参数的函数重写为一系列新函数,每个新函数仅使用一个参数。 编译器自动执行此操作,以纪念Haskell Curry(一位对功能编程的发展产生重大影响的数学家)的荣誉,它被称为“ currying ”。
为了了解实际中如何使用currying,让我们使用一个简单的代码示例来打印两个数字:
// let printTwoParameters xy = printfn "x=%iy=%i" xy
实际上,编译器以大约以下格式重写它:
// let printTwoParameters x = // let subFunction y = printfn "x=%iy=%i" xy // , subFunction //
更详细地考虑以下过程:
- 声明了一个名为“
printTwoParameters
”的函数,但仅接受一个参数:“ x”。 - 在其中创建一个局部函数,该局部函数也仅接受一个参数:“ y”。 请注意,局部函数使用参数“ x”,但是x不会作为参数传递给它。 “ x”的范围如此之大,以至于嵌套函数可以看到它并使用它而无需传递它。
- 最后,返回新创建的局部函数。
- 然后将返回的函数应用于参数“ y”。 参数“ x”在其中关闭,因此返回的函数仅需要参数“ y”即可完成其逻辑。
通过以这种方式重写函数,编译器可确保每个函数根据需要仅接受一个参数。 因此,使用“ printTwoParameters
”,您可能会认为这是一个具有两个参数的函数,但实际上仅使用了一个参数的函数。 您可以通过仅传递一个参数而不是两个参数来验证这一点:
// printTwoParameters 1 // val it : (int -> unit) = <fun:printTwoParameters@286-3>
如果使用一个参数进行计算,则不会出现错误-该函数将被返回。
因此,这是使用两个参数调用printTwoParameters
时实际发生的情况:
- 使用第一个参数(x)
printTwoParameters
printTwoParameters
返回一个新函数,其中“ x”被关闭。- 然后使用第二个参数(y)调用新函数
这是逐步和普通版本的示例:
// let x = 6 let y = 99 let intermediateFn = printTwoParameters x // - // x let result = intermediateFn y // let result = (printTwoParameters x) y // let result = printTwoParameters xy
这是另一个示例:
// let addTwoParameters xy = x + y // let addTwoParameters x = // ! let subFunction y = x + y // subFunction // // let x = 6 let y = 99 let intermediateFn = addTwoParameters x // - // x let result = intermediateFn y // let result = addTwoParameters xy
同样,“具有两个参数的函数”实际上是具有一个参数的函数,它返回一个中间函数。
但是等等, +
运算符呢? 这是必须带有两个参数的二进制操作吗? 不,它也像其他功能一样被管理。 这是一个名为“ +
”的函数,它addTwoParameters
一个参数并返回一个新的中间函数,就像上面的addTwoParameters
一样。
当我们编写表达式x+y
,编译器将代码重新排序 ,以将中缀转换为(+) xy
, 这是一个名为+
的函数 , 带有两个参数。 请注意,“ +”函数需要用括号括起来,以表示它被用作常规函数,而不是用作中缀运算符。
最后,具有两个参数的函数+
,被视为与具有两个参数的任何其他函数一样。
// let x = 6 let y = 99 let intermediateFn = (+) x // "" "" let result = intermediateFn y // let result = (+) xy // let result = x + y
是的,这适用于所有其他运算符和诸如printf
类的内置printf
。
// let result = 3 * 5 // - let intermediateFn = (*) 3 // "" 3 let result = intermediateFn 5 // printfn let result = printfn "x=%iy=%i" 3 5 // printfn - let intermediateFn = printfn "x=%iy=%i" 3 // "3" let result = intermediateFn 5
咖喱函数签名
现在我们知道了咖喱函数的工作原理,很有趣的是知道它们的签名会是什么样子。
回到第一个示例“ printTwoParameter
”,我们看到该函数接受一个参数并返回一个中间函数。 中间函数也接受一个参数,但不返回任何值(即unit
)。 因此,中间函数的类型为int->unit
。 换句话说,域printTwoParameters
为int
,范围为int->unit
。 放在一起,我们将看到最终的签名:
val printTwoParameters : int -> (int -> unit)
如果您计算显式管理的实现,则可以在签名中看到括号,但是如果您计算普通的隐式管理的实现,则不会有括号:
val printTwoParameters : int -> int -> unit
支架是可选的。 但是它们可以在头脑中表示出来,以简化对功能签名的感知。
返回中间函数的函数和带有两个参数的常规函数之间的区别是什么?
这是一个带有一个参数的函数,该函数返回另一个函数:
let add1Param x = (+) x // signature is = int -> (int -> int)
这是一个带有两个参数的函数,这些函数返回一个简单值:
let add2Params xy = (+) xy // signature is = int -> int -> int
它们的签名稍有不同,但是在实际意义上它们之间没有太大区别,除了第二个功能是自动管理的。
具有两个以上参数的功能
具有两个以上参数的函数的curring如何工作? 以相同的方式:对于每个参数,除最后一个参数外,该函数返回一个中间函数,该中间函数关闭前一个参数。
考虑这个困难的例子。 我已经明确声明了参数类型,但是该函数什么也不做。
let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () // let intermediateFn1 = multiParamFn 42 // multoParamFn int (bool -> string -> float -> unit) // intermediateFn1 bool // (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2 string // (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float // (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 // 2 int int string->bool->int // string, - bool, // int int->string->bool->unit // (int,string,bool) // (unit) (int->string)->int // , // ( int string) // int (int->string)->(int->bool) // (int string) // (int bool)
多参数难点
除非您了解currying背后的逻辑,否则它会产生一些意外的结果。 请记住,如果使用少于预期数量的参数运行该函数,则不会出错。 相反,您会得到部分应用的功能。 如果然后在期望该值的上下文中使用部分应用的函数,则可能会从编译器中获得晦涩的错误。
乍一看,考虑一个无害的功能:
// let printHello() = printfn "hello"
如下图所示,您认为会发生什么? 将“ hello”打印到控制台吗? 在执行之前尝试猜测。 提示:看一下函数的签名。
// printHello
与期望相反,不会有电话。 原始函数期望unit
作为尚未传递的参数。 因此,获得了部分应用的函数(在这种情况下,没有参数)。
那这种情况呢? 会被编译吗?
let addXY xy = printfn "x=%iy=%i" x x + y
如果运行它,编译器将抱怨printfn
这一行。
printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit.
如果不了解curry,则此消息可能非常含糊。 事实是,所有单独求值的表达式(即不用作返回值或通过“ let”绑定到某物) 必须在unit
值中求值。 在这种情况下,它不是以unit
值计算的,而是返回一个函数。 这是说printfn
缺少参数的很长的路要走。
在大多数情况下,与.NET世界中的库进行交互时,会发生此类错误。 例如, TextReader
类的Readline
方法必须带有一个unit
参数。 您常常会忘记这一点,而不用放在方括号中,在这种情况下,您在“调用”时不会出现编译器错误,但是当您尝试将结果解释为字符串时,它将出现。
let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // , printfn "The line is %s" line1 // // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //
在上面的代码中, line1
只是Readline
方法的指针或委托,而不是您可能期望的字符串。 在reader.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 // let x = intermediateFn 3 //intermediateFn ! // ==> error FS0003: This value is not a function // and cannot be applied
其他资源
F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:
还介绍了其他几种开始学习F#的方法 。
最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!
不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:
关于翻译作者
由@kleidemos翻译
在F#开发人员的俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss和@shwars为本文准备发表。