功能思维。 第7部分

我们将继续介绍有关F#中的函数式编程的系列文章。 今天,我们有一个非常有趣的话题:函数的定义。 其中,让我们讨论匿名函数,不带参数的函数,递归函数,组合器等等。 看猫下!




功能定义


我们已经知道如何使用“ let”语法创建常规函数:


let add xy = x + y 

在本文中,我们将介绍创建函数的其他方法,以及定义函数的技巧。


匿名函数(lambdas)


如果您熟悉其他语言的lambda,则以下几段看起来很熟悉。 匿名函数(或“ lambda表达式”)的定义如下:


 fun parameter1 parameter2 etc -> expression 

与C#中的lambda相比,有两个区别:


  • lambdas应该以fun关键字开头,这在C#中不是必需的
  • 使用单箭头-> ,而不是C#中的double =>

Lambda加法函数的定义:


 let add = fun xy -> x + y 

传统形式的功能相同:


 let add xy = x + y 

Lambda通常以小表达式的形式使用,或者在不希望为表达式定义单独的函数时使用。 如您所见,在使用列表时,这并不罕见。


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

请注意,必须在lambda周围使用括号。


当需要明显不同的功能时,也可以使用Lambda。 例如,可以使用lambdas重写前面讨论的“ adderGenerator ”(我们之前讨论过)


 //   let adderGenerator x = (+) x //     let adderGenerator x = fun y -> x + y 

lambda版本稍长一些,但是立即使您很清楚将返回一个中间函数。


Lambda可以嵌套。 adderGenerator定义的另一个示例,这次仅在lambda上。


 let adderGenerator = fun x -> (fun y -> x + y) 

您是否清楚所有三个定义都相同?


 let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y) 

如果不是,请重新阅读有关curring章节 。 这对于理解非常重要!


模式匹配


定义函数后,可以像上面的示例中那样显式地将参数传递给它,但是也可以直接在参数部分与模板进行比较。 换句话说,参数部分可能包含模式(匹配模式),而不仅仅是标识符!


以下示例演示了在函数定义中使用模式:


 type Name = {first:string; last:string} //    let bob = {first="bob"; last="smith"} //   //     let f1 name = //   let {first=f; last=l} = name //     printfn "first=%s; last=%s" fl //   let f2 {first=f; last=l} = //        printfn "first=%s; last=%s" fl //  f1 bob f2 bob 

仅当对应关系始终可确定时,才可以进行这种类型的比较。 例如,您不能以这种方式匹配联合类型和列表,因为某些情况无法匹配。


 let f3 (x::xs) = //       printfn "first element is=%A" x 

编译器将发出有关不完全匹配的警告(空白列表将在此函数的入口处导致运行时错误)。


常见错误:元组与 许多参数


如果您来自类似C的语言,则用作该函数唯一参数的元组可能会非常类似于多参数函数。 但这不是同一回事! 如前所述,如果您看到逗号,则很可能是元组。 参数以空格分隔。


混淆示例:


 //      let addTwoParams xy = x + y //      -  let addTuple aTuple = let (x,y) = aTuple x + y //         //        let addConfusingTuple (x,y) = x + y 

  • 第一个定义“ addTwoParams ”采用两个参数,以空格分隔。
  • 第二个定义“ addTuple ”采用一个参数。 此参数将元组中的“ x”和“ y”绑定并求和。
  • 第三个定义“ addConfusingTuple ”采用单个参数,例如“ addTuple ”,但是窍门是将这个元组解包(与模式匹配)并使用模式匹配将其绑定为参数定义的一部分。 在幕后,所有操作都与addTuple完全相同。

让我们看一下签名(如果不确定某些内容,请始终查看它们)。


 val addTwoParams : int -> int -> int //   val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int 

现在在这里:


 // addTwoParams 1 2 // ok --      addTwoParams (1,2) // error -     // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b 

在这里,我们看到了第二个调用中的错误。


首先,编译器将(1,2)视为形式('a * 'b)的通用元组,尝试将其作为第一个参数传递给addTwoParams 。 之后,他抱怨期望的第一个参数addTwoParams不是int ,但是尝试传递一个元组。


要制作元组,请使用逗号!


 addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 //  , //  ! addTuple y // ok addConfusingTuple y // ok 

反之亦然,如果您将多个参数传递给等待元组的函数,您还将收到一个无法理解的错误。


 addConfusingTuple 1 2 // error --          // => error FS0003: This value is not a function and // cannot be applied 

这次,编译器决定一旦addConfusingTuple两个参数, addConfusingTuple应该对addConfusingTuple进行处理。 条目“ addConfusingTuple 1 ”是部分应用程序,应该返回一个中间函数。 尝试使用参数“ 2”调用此中间函数将引发错误,因为 没有中间功能! 我们看到了与curring一章中相同的错误,该章讨论了参数过多的问题。


为什么不使用元组作为参数?


上面对元组的讨论显示了定义具有许多参数的函数的另一种方法:可以将所有参数组装成一个结构,而不必分别传递它们。 在下面的示例中,该函数采用单个参数-三个元素的元组。


 let f (x,y,z) = x + y * z //  - int * int * int -> int //  f (1,2,3) 

应该注意的是,签名不同于具有三个参数的函数的签名。 只有一个箭头,一个参数和指向元组(int*int*int)星号。


什么时候需要提交带有单独参数的参数,以及何时使用元组?


  • 当元组本身很重要时。 例如,对于在三维空间中进行操作,三元组将比分别使用三个坐标更方便。
  • 有时,元组用于将必须存储在一起的数据合并到一个结构中。 例如,.NET库中的TryParse方法返回结果和一个布尔变量作为元组。 但是要存储大量相关数据,最好定义一个类或记录( record)

特殊情况:.NET库元组和函数


调用.NET库时,逗号非常常见!


它们都接受元组,并且调用看起来与C#中的相同:


 //  System.String.Compare("a","b") //   System.String.Compare "a" "b" 

原因是经典.NET的功能无法使用,无法部分应用。 所有参数必须始终立即传输,最明显的方法是使用元组。


请注意,这些调用仅看起来像传输元组,但这实际上是一种特殊情况。 您不能将真正的元组传递给以下函数:


 let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error 

如果要部分应用.NET函数,则可以像之前所做的那样或如下所示在它们上面编写包装器:


 //    let strCompare xy = System.String.Compare(x,y) //    let strCompareWithB = strCompare "B" //      ["A";"B";"C"] |> List.map strCompareWithB 

选择单个参数和分组参数的指南


元组的讨论引出了一个更笼统的话题:何时应该分开参数,何时将参数分组?


在这方面,您应注意F#与C#的区别。 在C#中, 总是传递所有参数,因此,这里根本不会出现这个问题! 在F#中,由于部分应用,只能表示某些参数,因此有必要区分参数应组合的情况和参数独立的情况。


有关在设计自己的功能时如何构造参数的一般建议。


  • 在一般情况下,最好使用单独的参数而不是传递一个结构(元组或记录)总是更好。 这允许更灵活的行为,例如部分应用程序。
  • 但是,当需要一次传递一组参数时,应使用某种分组机制。

换句话说,在开发功能时,问自己:“我可以单独提供此参数吗?” 如果答案为否,则应将参数分组。


让我们看几个例子:


 //     . //      ,       let add xy = x + y //         //      ,    let locateOnMap (xCoord,yCoord) = //  //      //      -     type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = //  let setCustomerName first last = //   //     //     //    ,     let setCustomerName myCredentials aName = // 

最后,确保参数的顺序对部分应用有所帮助(请参阅此处的手册)。 例如,为什么在最后一个函数aName放在myCredentials之前?


没有参数的功能


有时您可能需要一个不接受任何参数的函数。 例如,您需要可以多次调用的函数“ hello world”。 如上一节所示,天真定义不起作用。


 let sayHello = printfn "Hello World!" //      

但这可以通过在函数中添加单位参数或使用lambda来解决。


 let sayHello() = printfn "Hello World!" //  let sayHello = fun () -> printfn "Hello World!" //  

之后,应始终使用unit参数调用该函数:


 //  sayHello() 

与.NET库进行交互时经常发生的事情:


 Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory() 

请记住,使用unit参数调用它们!


定义新的运营商


您可以使用一个或多个运算符来定义函数(有关字符列表,请参阅文档 ):


 //  let (.*%) xy = x + y + 1 

您必须在字符周围使用括号来定义函数。


*开头的运算符需要在括号和*之间加一个空格,因为 在F#中(*用作注释的开头(如C#中的/*...*/ ):


 let ( *+* ) xy = x + y + 1 

定义后,如果将新功能括在方括号中,则可以按常规方式使用它:


 let result = (.*%) 2 3 

如果该函数与两个参数一起使用,则可以使用不带括号的中缀运算符记录。


 let result = 2 .*% 3 

您还可以定义以!开头的前缀运算符!~ (有一些限制,请参见文档


 let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello" 

在F#中,定义语句是一个相当常见的操作,许多库将导出名称,如>=><*>


无点式


我们已经看到了许多缺少最新参数以减少混乱程度的函数示例。 这种样式称为无点样式默认编程


以下是一些示例:


 let add xy = x + y //  let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 //  let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list //  let sum = List.reduce (+) // point free 

这种风格各有利弊。


优点之一是,重点在于高阶函数的组成,而不是对低级对象大惊小怪。 例如,“ (+) 1 >> (*) 2 ”是显式加法,后跟乘法。 并且“ List.reduce (+) ”清楚地表明,无论列表信息如何,加法操作都很重要。


无意义的样式使您可以专注于基本算法并识别代码中的常见功能。 上面使用的“ reduce ”功能就是一个很好的例子。 该主题将在计划的列表处理系列中进行讨论。


另一方面,过多使用这种样式会使代码晦涩难懂。 显式参数充当文档,其名称(例如“列表”)使您更容易理解函数的功能。


像编程中的所有内容一样,最好的建议是偏向提供最清晰的方法。


组合器


组合器 ”称为函数,其结果仅取决于其参数。 这意味着不依赖外部世界,尤其是没有其他功能或全局值可以影响它们。


实际上,这意味着组合功能以各种方式受到其参数组合的限制。


我们已经看到了几种组合器:管道和组合运算符。 如果查看它们的定义,那么很显然,他们所做的只是以各种方式对参数重新排序。


 let (|>) xf = fx //  pipe let (<|) fx = fx //  pipe let (>>) fgx = g (fx) //   let (<<) gfx = g (fx) //   

另一方面,“ printf”之类的函数尽管是原始函数,但它们并不是组合器,因为它们依赖于外部世界(I / O)。


组合鸟


组合器是整个逻辑部分(自然称为“组合逻辑”)的基础,该部分在计算机和编程语言问世之前就已经发明了很多年。 组合逻辑对函数式编程有很大的影响。


要了解有关组合器和组合逻辑的更多信息,我推荐Raymond Smullyan的书“模拟一只知更鸟”。 在其中,他解释了其他组合器,并幻想给它们起鸟名 。 以下是一些标准组合器及其鸟名的示例:


 let I x = x //  ,  Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling //   ... let rec Y fx = f (Y f) x // Y-,  Sage bird 

字母名称是非常标准的,因此您可以将K-combinator引用给熟悉此术语的任何人。


事实证明,可以通过这些标准组合器来表示许多常见的编程模式。 例如,Kestrel是流畅接口中的常规模式,您可以在其中执行某些操作,但返回原始对象。 画眉是管道,Queer是直接合成,Y组合器在创建递归函数方面做得很好。


实际上,有一个众所周知的定理 ,即仅使用两个基本组合器(Kestrel和Starling)就可以构造任何可计算函数。


组合图书馆


组合库是导出许多旨在共享的组合功能的库。 这样的库的用户可以轻松地将功能组合在一起,以轻松获得更大,更复杂的功能,例如多维数据集。


设计良好的组合器库使您可以专注于高级功能,并隐藏低级“噪声”。 我们已经在“为什么使用F#”系列的几个示例中看到了它们的功能,并且List模块中充斥着此类功能,如果您考虑一下,“ fold ”和“ map ”也是组合器。


组合器的另一个优点是它们是最安全的功能类型。 因为 它们不依赖外部世界;当全球环境变化时,它们也不会变化。 如果上下文发生更改,则读取全局值或使用库函数的函数可能会在调用之间中断或更改。 组合器永远不会发生这种情况。


在F#中,可使用组合器库进行解析(FParsec),创建HTML,测试框架等。 在下一个系列的后面,我们将讨论和使用组合器。


递归函数


函数通常需要从其主体引用自身。 一个经典的例子是斐波那契函数。


 let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

不幸的是,该函数将无法编译:


 error FS0039: The value or constructor 'fib' is not defined 

您必须使用rec关键字告诉编译器这是递归函数。


 let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

递归函数和数据结构在函数式编程中非常常见,我希望以后再专门讨论这个主题。


其他资源


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



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


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


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



关于翻译作者


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

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


All Articles