功能思维。 第4部分

简短介绍了基本类型之后,我们可以再次返回到函数。 特别是对于前面提到的谜语:如果一个数学函数只能采用一个参数,那么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 //   

更详细地考虑以下过程:


  1. 声明了一个名为“ printTwoParameters ”的函数,但仅接受一个参数:“ x”。
  2. 在其中创建一个局部函数,该局部函数也仅接受一个参数:“ y”。 请注意,局部函数使用参数“ x”,但是x不会作为参数传递给它。 “ x”的范围如此之大,以至于嵌套函数可以看到它并使用它而无需传递它。
  3. 最后,返回新创建的局部函数。
  4. 然后将返回的函数应用于参数“ 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 。 换句话说,域printTwoParametersint ,范围为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为本文准备发表。

Source: https://habr.com/ru/post/zh-CN430620/


All Articles