这已经是有关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
嵌套函数不需要参数,因为 她可以直接访问n
和max
。
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
,该原始MathStuff
被FloatLib
模块阻止(隐藏)。
结果,我们得到了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访问控制运算符,例如public
, private
和internal
。 MSDN文章包含完整的信息。
- 这些访问说明符可以应用于(“绑定”)顶级功能,值,类型和模块中的其他声明。 也可以为模块本身指定它们(例如,可能需要专用的嵌套模块)。
- 默认情况下,所有内容都具有公共访问权限(几种情况除外),因此要保护它们,您需要使用
private
或internal
。
这些访问说明符只是控制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
,全名add
是Utilities.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.Func
和MyModule.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为本文准备发表。