功能思维。 第5部分

在上一篇有关curring的文章中,我们看到了如何将具有多个参数的函数拆分为具有一个参数的较小函数。 这是数学上正确的解决方案,但这样做还有其他原因-它还导致了一种非常强大的技术,称为函数的部分应用 。 这种样式在函数式编程中非常广泛地使用,理解它非常重要。




部分使用功能


局部应用程序的想法是,如果我们固定函数的前N个参数,则会获得带有其余参数的新函数。 通过对currying的讨论,可以看到部分应用程序是如何自然发生的。
一些简单的例子来说明:


//  ""       +  42 let add42 = (+) 42 //    add42 1 add42 3 //       //      [1;2;3] |> List.map add42 //          "" let twoIsLessThan = (<) 2 //   twoIsLessThan 1 twoIsLessThan 3 //      twoIsLessThan [1;2;3] |> List.filter twoIsLessThan //   ""       printfn let printer = printfn "printing param=%i" //      printer    [1;2;3] |> List.iter printer 

在每种情况下,我们都会创建一个部分应用的函数,该函数可以在不同情况下重用。


当然,部分应用程序使固定功能参数同样容易。 以下是一些示例:


 //    List.map let add1 = (+) 1 let add1ToEach = List.map add1 //   "add1"  List.map //  add1ToEach [1;2;3;4] //    List.filter let filterEvens = List.filter (fun i -> i%2 = 0) //    //  filterEvens [1;2;3;4] 

以下更复杂的示例说明了如何使用相同的方法来透明地创建“嵌入式”行为。


  • 我们创建了一个将两个数字相加的函数,但除此之外,它还需要一个记录函数来记录这些数字和结果。
  • 记录功能采用两个参数:(字符串)“名称”和(通用)“值”,因此它具有签名string->'a->unit
  • 然后,我们创建记录功能的各种实现,例如控制台记录器或基于弹出窗口的记录器。
  • 最后,我们使用封闭的记录器部分应用main函数来创建新函数。

 //      - let adderWithPluggableLogger logger xy = logger "x" x logger "y" y let result = x + y logger "x+y" result result //  -      let consoleLogger argName argValue = printfn "%s=%A" argName argValue //           let addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger 1 2 addWithConsoleLogger 42 99 //  -      let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore //    -       let addWithPopupLogger = adderWithPluggableLogger popupLogger addWithPopupLogger 1 2 addWithPopupLogger 42 99 

这些封闭记录器功能可以像其他任何功能一样使用。 例如,我们可以创建一个用于添加42的部分应用程序,然后将其传递给list函数,就像对简单的add42函数所做的add42


 //         42 let add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger [1;2;3] |> List.map add42 //     

部分应用的功能是一个非常有用的工具。 我们可以创建灵活的(尽管很复杂)库函数,并且很容易使它们在默认情况下可重用,从而可以从客户端代码中隐藏复杂性。


部分功能设计


显然,参数的顺序会严重影响部分使用的便利性。 例如, List大多数函数(例如List.mapList.filter具有类似的形式,即:


 List-function [function parameter(s)] [list] 

该列表始​​终是最后一个参数。 完整形式的一些示例:


 List.map (fun i -> i+1) [0;1;2;3] List.filter (fun i -> i>1) [0;1;2;3] List.sortBy (fun i -> -i ) [0;1;2;3] 

使用部分应用程序的相同示例:


 let eachAdd1 = List.map (fun i -> i+1) eachAdd1 [0;1;2;3] let excludeOneOrLess = List.filter (fun i -> i>1) excludeOneOrLess [0;1;2;3] let sortDesc = List.sortBy (fun i -> -i) sortDesc [0;1;2;3] 

如果使用不同顺序的参数实现库函数,则部分应用程序将不太方便。


当您使用许多参数编写函数时,可以考虑它们的最佳顺序。 与所有设计问题一样,没有“正确”的答案,但是有一些普遍接受的建议。


  1. 从可能是静态的选项开始。
  2. 最后设置数据结构或集合(或其他更改的参数)
  3. 为了更好地了解减法等操作,建议遵守预期顺序

第一个技巧很简单。 像上面记录器中的示例一样,应该由部分应用程序“固定”的参数应该放在第一位。


紧随第二个技巧,可以更轻松地使用流水线运算符和组合。 在上面列出的函数的示例中,我们已经多次看到了这一点。


 //          let result = [1..10] |> List.map (fun i -> i+1) |> List.filter (fun i -> i>5) 

类似地,列表中的部分应用功能很容易暴露于合成,因为 list参数可以省略:


 let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10] 

部分BCL功能包装


可以从F#轻松访问.NET的.NET基类库(BCL)函数,但是它们的设计不依赖于诸如F#之类的功能语言。 例如,大多数函数的开头都需要一个数据参数,而在F#中,该数据参数通常应该是最后一个。


但是,您可以轻松编写包装程序以使这些功能更加惯用。 在下面的示例中,.NET字符串函数被重写,以便最后而不是首先使用目标字符串:


 //     .NET  let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor) 

在字符串成为最后一个参数之后,您可以照常在管道中使用这些函数:


 let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f") 

或在功能组成上:


 let compositeOp = replace "h" "j" >> startsWith "j" let result = compositeOp "hello" 

了解输送机操作员


在看到业务中的部分应用程序之后,您可以了解流水线功能如何工作。


流水线功能定义如下:


 let (|>) xf = fx 

她所做的只是在函数之前而不是后面加一个参数。


 let doSomething xyz = x+y+z doSomething 1 2 3 //     

在函数f具有多个参数的情况下,函数f的最后一个参数将用作管道的输入值x 。 实际上,传递函数f已经被部分应用,并且只需要一个参数-流水线的输入值(te x )。


这是为部分使用而重写的类似示例。


 let doSomething xy = let intermediateFn z = x+y+z intermediateFn //  intermediateFn let doSomethingPartial = doSomething 1 2 doSomethingPartial 3 //       3 |> doSomethingPartial //    ,        

如您所见,流水线运算符在F#中极为常见,每当您要保留自然数据流时就使用它。 您可能还会遇到其他一些示例:


 "12" |> int //   "12"  int 1 |> (+) 2 |> (*) 3 //   

反向输送机操作员


有时,您可能会遇到反向管道运算符“ <|”。


 let (<|) fx = fx 

此功能似乎无能为力,为什么它存在?


原因是当将逆流水线运算符用作infix样式的二进制运算符时,它减少了对括号的需要,这使代码更整洁。


 printf "%i" 1+2 //  printf "%i" (1+2) //   printf "%i" <| 1+2 //    

您可以一次在两个方向上使用管道来获取伪中缀表示法。


 let add xy = x + y (1+2) add (3+4) //  1+2 |> add <| 3+4 //   

其他资源


F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:



还介绍了其他几种开始学习F#的方法


最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!


不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:



关于翻译作者


@kleidemos翻译
在F#开发人员俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss@shwars为本文准备发表。

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


All Articles