功能思维。 第9部分

这已经是有关F#!函数编程的系列文章的第9部分。 我确信在哈布雷(Habré)上不会有这么长的周期。 但是我们不会停止。 今天,我们将讨论嵌套函数,模块,名称空间以及模块中混合类型和函数。






现在您知道如何定义函数,但是如何组织它们呢?


F#具有三个选项:


  • 函数可以嵌套在其他函数中。
  • 在应用程序级别,顶级功能被分组为“模块”。
  • 或者您可以遵循面向对象的方法,并将函数作为方法附加到类型上。

在本文中,我们将考虑前两种方法,其余的将在下一种方法中使用。


嵌套函数


在F#中,可以在其他函数中定义函数。 这是封装辅助功能的好方法,这些辅助功能仅是主要功能所需要的,并且不应从外部看到。


在下面的示例中, add嵌套在addThreeNumbers


 let addThreeNumbers xyz = //     let add n = fun x -> x + n //    x |> add y |> add z addThreeNumbers 2 3 4 

嵌套函数可以直接访问父参数,因为它们在其范围内。
因此,在下面的示例中, printError嵌套函数不需要参数,因为 她可以直接访问nmax


 let validateSize max n = //       let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max //    if n > max then printError() validateSize 10 9 validateSize 10 11 

一个非常常见的模式是定义嵌套的递归辅助函数的主要函数,该函数以相应的初始值调用。
以下是此类代码的示例:


 let sumNumbersUpTo max = //      let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) //       recursiveSum max 0 sumNumbersUpTo 10 

尝试避免深度嵌套,尤其是在直接访问父变量(而不是参数形式)的情况下。
与许多嵌套的命令分支中最糟糕的嵌套一样,过度嵌套的函数将很难理解。


一个不怎么做的例子:


 // wtf,    ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x 

模组


模块只是将功能组合在一起的一个集合,通常是因为它们使用相同的一种或多种数据类型。


模块定义与功能定义非常相似。 它以module关键字开头,然后是=符号,然后是模块的内容。
模块的内容以及函数定义中的表达式必须设置有偏移量的格式。


包含两个功能的模块的定义:


 module MathStuff = let add xy = x + y let subtract xy = x - y 

如果在Visual Studio中打开此代码, add鼠标悬停在add您会看到全名add ,实际上等于MathStuff.add ,就像MastStuff是一个类,而add是一个方法一样。


实际上,这正是正在发生的事情。 在后台,F#编译器使用静态方法创建一个静态类。 等效的C#看起来像这样:


 static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } } 

认识到模块只是静态类而函数是静态方法,可以很好地理解模块在F#中的工作方式,因为大多数适用于静态类的规则也适用于模块。


就像在C#中一样,每个独立功能都应该是该类的一部分,在F#中,每个独立功能都应该是该模块的一部分。


访问模块外部的功能


如果需要从另一个模块访问功能,则可以通过其全名来引用它。


 module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = //     MathStuff let add1 x = MathStuff.add x 1 

您也可以使用open指令导入另一个模块的所有功能,然后可以使用短名称代替完整名称。


 module OtherStuff = open MathStuff //      let add1 x = add x 1 

期望使用名称的规则。 您始终可以按功能的全名来访问功能,也可以根据当前作用域使用相对或不完整的名称。


嵌套模块


与静态类一样,模块可以包含嵌套模块:


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

其他模块可以根据需要使用全名或相对名来引用嵌套模块中的函数:


 module OtherStuff = open MathStuff let add1 x = add x 1 //   let add1Float x = MathStuff.FloatLib.add x 1.0 //   let sub1Float x = FloatLib.subtract x 1.0 

顶级模块


因此,由于模块可以嵌套,因此在上链之前,您可以到达顶层的某些父模块。 真的是


与前面显示的模块不同,顶层模块的定义有所不同。


  • module MyModuleName 必须是文件中的第一个声明
  • 标志=缺少
  • 模块内容不得缩进

通常,每个源.FS文件中都应存在一个“顶级”声明。 有一些例外,但这仍然是一种好习惯。 模块名称不必与文件名匹配,但是两个文件不能包含名称相同的模块。


对于.FSX文件,不需要模块声明,在这种情况下,脚本文件名将自动成为模块名。


声明为“顶部模块”模块的MathStuff的示例:


 //    module MathStuff let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

请注意,“顶层”代码( module MathStuff )中没有缩进,而嵌套FloatLib模块的内容仍然必须缩进。


其他模块内容


除功能外,模块还可以包含其他声明,例如类型声明,简单值和初始化代码(例如,静态构造函数)


 module MathStuff = //  let add xy = x + y let subtract xy = x - y //   type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg //  /   do printfn "module initialized" 

顺便说一下,如果以交互方式运行这些示例,则可能需要足够频繁地重新启动会话,以使代码保持“新鲜”且不会被先前的计算所感染。


隐藏(重叠,阴影)


这也是我们的示例模块。 请注意, MathStuff包含add函数以及 FloatLib


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

如果在当前作用域中打开两个模块并调用add什么?


 open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int 

碰巧, MathStuff.FloatLib模块重新定义了原始MathStuff ,该原始MathStuffFloatLib模块阻止(隐藏)。


结果,我们得到了FS0001编译器错误,因为第一个参数1被期望为浮点数。 要解决此问题,您必须将1更改为1.0


不幸的是,在实践中这是谨慎而又容易被忽视的。 有时使用这种技术,您可以做一些有趣的技巧,就像子类一样,但是大多数情况下,具有相同名称的函数很烦人(例如,在极为通用的map函数的情况下)。


如果要避免此行为,可以使用RequireQualifiedAccess属性来停止它。 在同一示例中,两个模块都使用此属性进行修饰:


 [<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y //   [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

现在, open指令不可用:


 open MathStuff //  open MathStuff.FloatLib //  

但是您仍然可以通过函数的全名来访问函数(没有任何歧义):


 let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0 

门禁控制


F#支持使用标准.NET访问控制运算符,例如publicprivateinternalMSDN文章包含完整的信息。


  • 这些访问说明符可以应用于(“绑定”)顶级功能,值,类型和模块中的其他声明。 也可以为模块本身指定它们(例如,可能需要专用的嵌套模块)。
  • 默认情况下,所有内容都具有公共访问权限(几种情况除外),因此要保护它们,您需要使用privateinternal

这些访问说明符只是控制F#中可见性的一种方法。 一种完全不同的方法是使用类似于C头文件的签名文件,它们抽象地描述了模块的内容。 签名对于认真的封装非常有用,但是要考虑它们的功能,您将必须等待计划的基于功能的系列封装和安全性


命名空间


F#中的命名空间类似于C#中的命名空间。 它们可用于组织模块和类型,以避免名称冲突。


使用namespace关键字声明的namespace


 namespace Utilities module MathStuff = //  let add xy = x + y let subtract xy = x - y 

由于MathStuff这个命名空间, MathStuff模块的全名MathStuff成为Utilities.MathStuff ,全名addUtilities.MathStuff.add


相同的缩进规则适用于上面显示的模块名称空间中的模块。


您还可以通过在模块名称中添加句点来显式声明名称空间。 即 上面的代码可以这样重写:


 module Utilities.MathStuff //  let add xy = x + y let subtract xy = x - y 

MathStuff模块的全名仍然是Utilities.MathStuff ,但现在它是一个顶级模块,其内容不需要缩进。


使用名称空间的一些其他功能:


  • 命名空间对于模块是可选的。 与C#不同,对于F#项目,没有默认的名称空间,因此没有名称空间的顶级模块将是全局的。 如果计划创建可重用的库,则必须添加几个名称空间,以避免与其他库的代码冲突。
  • 命名空间可以直接包含类型声明,但不能包含函数声明。 如前所述,所有函数和值声明必须是模块的一部分。
  • 最后,请记住,名称空间在脚本中不起作用。 例如,如果您尝试将名称空间声明(例如namespace Utilities )发送到交互式窗口,则会收到错误。

命名空间层次结构


您可以通过简单地用点将名称分隔来建立名称空间的层次结构:


 namespace Core.Utilities module MathStuff = let add xy = x + y 

如果需要,还可以在一个文件中声明两个名称空间。 应该注意的是,所有名称空间都必须以其全名声明-它们不支持嵌套。


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y 

命名空间和模块之间的名称冲突是不可能的。


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core //    - Core.Utilities //     ! module Utilities = let add xy = x + y 

模块中的混合类型和功能


如我们所见,模块通常由许多与特定数据类型交互的相互依赖的函数组成。


在OOP中,它们之上的数据结构和功能将在一个类中组合在一起。 在功能性F#中,其上方的数据结构和功能被组合为一个模块。


有两种将类型和功能组合在一起的模式:


  • 类型与函数分开声明
  • 类型在与函数相同的模块中声明

在第一种情况下,在任何模块外部 (但在名称空间中)声明该类型,然后将与此类型一起使用的功能放入相同类型的模块中。


 //    namespace Example //      type PersonType = {First:string; Last:string} //    ,     module Person = //  let create first last = {First=first; Last=last} // ,     let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s" 

或者,类型模块内部声明并具有简单的名称,例如“ T ”或模块名称。 对函数的访问大致如下: MyModule.FuncMyModule.Func2 ,并访问类型: MyModule.T


 module Customer = // Customer.T -      type T = {AccountId:int; Name:string} //  let create id name = {T.AccountId=id; T.Name=name} // ,     let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b" 

请注意,在两种情况下,都必须有一个构造函数来创建类型(工厂)的新实例。 然后,在客户端代码中,您几乎不必显式访问类型名称,也不必怀疑类型是否在模块内部。


那么选择哪种方式呢?


  • 第一种方法更像经典的.NET,如果计划将此库用于F#之外的代码(应该使用单独存在的类),则应首选该方法。
  • 第二种方法在其他功能语言中更为常见。 模块内部的类型将作为嵌套类进行编译,这对于OOP语言通常不是很方便。

您可以自己尝试两种方法。 在团队发展的情况下,必须选择一种风格。


仅包含类型的模块


如果有许多类型需要声明而没有任何函数,请不要使用该模块。 您可以直接在名称空间中声明类型,而无需使用嵌套类。


例如,您可能要这样做:


 //    module Example //     type PersonType = {First:string; Last:string} //    ,  ... 

这是另一种方法。 单词module仅用单词namespace替换。


 //    namespace Example //     type PersonType = {First:string; Last:string} 

在这两种情况下, PersonType都将具有相同的全名。


请注意,此替换仅适用于类型。 必须始终在模块内部声明函数。


其他资源


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



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


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


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



关于翻译作者


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

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


All Articles