F# 编程/模块和命名空间
F# : 模块和命名空间 |
模块和命名空间主要用于代码的组织和分组。
不需要任何代码来定义模块。如果一个代码文件不包含前导 namespace
或 module
声明,F# 代码会隐式地将代码置于一个模块中,模块名称与文件名相同,首字母大写。
要访问另一个模块中的代码,只需使用 .
符号:moduleName.member
。请注意,这种表示法类似于访问静态成员 的语法 — 这并非巧合。F# 模块被编译为只包含静态成员、值和类型定义的类。
让我们创建两个文件
DataStructures.fs
type 'a Stack =
| EmptyStack
| StackNode of 'a * 'a Stack
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
Program.fs
let x =
DataStructures.StackNode(1,
DataStructures.StackNode(2,
DataStructures.StackNode(3, DataStructures.EmptyStack)))
let y = DataStructures.getRange 5 10
printfn "%A" x
printfn "%A" y
该程序输出
StackNode (1,StackNode (2,StackNode (3,EmptyStack))) StackNode (5, StackNode (6,StackNode (7,StackNode (8,StackNode (9,StackNode (10,EmptyStack))))))
- 注意: 请记住,F# 中的编译顺序很重要。依赖项必须在被依赖项之前,因此在编译该程序时,
DataStructures.fs
必须位于Program.fs
之前。
像所有模块一样,我们可以使用 open
关键字来访问模块中的方法,而无需完全限定方法的命名。这允许我们修改 Program.fs
如下
open DataStructures
let x = StackNode(1, StackNode(2, StackNode(3, EmptyStack)))
let y = getRange 5 10
printfn "%A" x
printfn "%A" y
使用 module
关键字很容易创建子模块
(* DataStructures.fs *)
type 'a Stack =
| EmptyStack
| StackNode of 'a * 'a Stack
module StackOps =
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
由于 getRange
方法位于另一个模块下,因此该方法的全限定名称为 DataStructures.StackOps.getRange
。我们可以像下面这样使用它
(* Program.fs *)
open DataStructures
let x =
StackNode(1, StackNode(2, StackNode(3, EmptyStack)))
let y = StackOps.getRange 5 10
printfn "%A" x
printfn "%A" y
F# 允许我们创建同名的模块和类型,例如以下代码是完全可以接受的
type 'a Stack =
| EmptyStack
| StackNode of 'a * 'a Stack
module Stack =
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
- 注意: 可以将子模块嵌套在其他子模块中。但是,作为一个通用的原则,最好避免创建复杂的模块层次结构。函数式编程库往往非常“扁平”,几乎所有功能都可以在层次结构的前 2 或 3 级访问。这与许多其他 OO 语言鼓励程序员创建深度嵌套的类库形成对比,在这些库中,功能可能被埋藏在层次结构的 8 或 10 级之下。
F# 支持扩展方法,允许程序员向类和模块添加新的静态和实例方法,而无需从它们继承。
扩展模块
Seq
模块 包含几对方法
iter
/iteri
map
/mapi
Seq
有一个 forall
成员,但没有对应的 foralli
函数,它包含每个序列元素的索引。我们只需创建一个同名模块即可将此缺失的方法添加到模块中。例如,使用 fsi
> module Seq =
let foralli f s =
s
|> Seq.mapi (fun i x -> i, x) (* pair item with its index *)
|> Seq.forall (fun (i, x) -> f i x) (* apply item and index to function *)
let isPalindrome (input : string) =
input
|> Seq.take (input.Length / 2)
|> Seq.foralli (fun i x -> x = input.[input.Length - i - 1]);;
module Seq = begin
val foralli : (int -> 'a -> bool) -> seq<'a> -> bool
end
val isPalindrome : string -> bool
> isPalindrome "hello";;
val it : bool = false
> isPalindrome "racecar";;
val it : bool = true
扩展类型
System.String
有许多有用的方法,但假设我们认为它缺少几个重要的函数,Reverse
和 IsPalindrome
。由于该类被标记为 sealed
或 NotInheritable
,因此我们无法创建该类的派生版本。相反,我们创建一个包含我们想要的新方法的模块。以下是在 fsi 中演示如何向 String
类添加新的静态和实例方法的示例
> module Seq =
let foralli f s =
s
|> Seq.mapi (fun i x -> i, x) (* pair item with its index *)
|> Seq.forall (fun (i, x) -> f i x) (* apply item and index to function *)
module StringExtensions =
type System.String with
member this.IsPalindrome =
this
|> Seq.take (this.Length / 2)
|> Seq.foralli (fun i x -> this.[this.Length - i - 1] = x)
static member Reverse(s : string) =
let chars : char array =
let temp = Array.zeroCreate s.Length
let charsToTake = if temp.Length % 2 <> 0 then (temp.Length + 1) / 2 else temp.Length / 2
s
|> Seq.take charsToTake
|> Seq.iteri (fun i x ->
temp.[i] <- s.[temp.Length - i - 1]
temp.[temp.Length - i - 1] <- x)
temp
new System.String(chars)
open StringExtensions;;
module Seq = begin
val foralli : (int -> 'a -> bool) -> seq<'a> -> bool
end
module StringExtensions = begin
end
> "hello world".IsPalindrome;;
val it : bool = false
> System.String.Reverse("hello world");;
val it : System.String = "dlrow olleh"
默认情况下,模块中的所有成员都可以在模块外部访问。但是,模块通常包含不应在模块外部访问的成员,例如辅助函数。公开模块成员子集的一种方法是为该模块创建签名文件。(另一种方法是对单个声明应用 .Net CLR 访问修饰符,例如 public、internal 或 private)。
签名文件与相应的模块同名,但以“.fsi”扩展名结尾(f-sharp 接口)。签名文件始终位于相应的实现文件之前,实现文件具有相应的“.fs”扩展名。例如
DataStructures.fsi
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
module Stack =
val getRange : int -> int -> int stack
val hd : 'a stack -> 'a
val tl : 'a stack -> 'a stack
val fold : ('a -> 'b -> 'a) -> 'a -> 'b stack -> 'a
val reduce : ('a -> 'a -> 'a) -> 'a stack -> 'a
DataStructures.fs
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
module Stack =
(* helper functions *)
let internal_head_tail = function
| EmptyStack -> failwith "Empty stack"
| StackNode(hd, tail) -> hd, tail
let rec internal_fold_left f acc = function
| EmptyStack -> acc
| StackNode(hd, tail) -> internal_fold_left f (f acc hd) tail
(* public functions *)
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
let hd s = internal_head_tail s |> fst
let tl s = internal_head_tail s |> snd
let fold f seed stack = internal_fold_left f seed stack
let reduce f stack = internal_fold_left f (hd stack) (tl stack)
Program.fs
open DataStructures
let x = Stack.getRange 1 10
printfn "%A" (Stack.hd x)
printfn "%A" (Stack.tl x)
printfn "%A" (Stack.fold ( * ) 1 x)
printfn "%A" (Stack.reduce ( + ) x)
(* printfn "%A" (Stack.internal_head_tail x) *) (* will not compile *)
由于 Stack.internal_head_tail
未在我们的接口文件中定义,因此该方法被标记为 private
,并且不再可以在 DataStructures
模块之外访问。
模块签名对于构建代码库的骨架很有用,但是它们有一些注意事项。如果要通过签名在模块中公开类、记录或联合体,那么签名文件必须公开所有对象成员、记录字段和联合体的案例。此外,在模块中定义的函数及其在签名文件中的相应签名必须完全匹配。与 OCaml 不同,F# 不允许模块中的函数使用泛型类型 'a -> 'a -> 'a
在签名文件中被限制为 int -> int -> int
。
命名空间是模块、类和其他命名空间的层次结构分类。例如,System.Collections
命名空间将 .NET BCL 中的所有集合和数据结构分组在一起,而 System.Security.Cryptography
命名空间将所有提供加密服务的类分组在一起。
命名空间主要用于避免名称冲突。例如,假设我们正在编写一个将多个供应商的代码合并在一起的应用程序。如果供应商 A 和供应商 B 都有一个名为 Collections.Stack
的类,而我们写了代码 let s = new Stack()
,编译器如何知道我们想要创建哪个堆栈?命名空间可以通过向我们的代码添加一层分组来消除这种歧义。
使用 namespace
关键字将代码分组在命名空间下
DataStructures.fsi
namespace Princess.Collections
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
module Stack =
val getRange : int -> int -> int stack
val hd : 'a stack -> 'a
val tl : 'a stack -> 'a stack
val fold : ('a -> 'b -> 'a) -> 'a -> 'b stack -> 'a
val reduce : ('a -> 'a -> 'a) -> 'a stack -> 'a
DataStructures.fs
namespace Princess.Collections
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
module Stack =
(* helper functions *)
let internal_head_tail = function
| EmptyStack -> failwith "Empty stack"
| StackNode(hd, tail) -> hd, tail
let rec internal_fold_left f acc = function
| EmptyStack -> acc
| StackNode(hd, tail) -> internal_fold_left f (f acc hd) tail
(* public functions *)
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
let hd s = internal_head_tail s |> fst
let tl s = internal_head_tail s |> snd
let fold f seed stack = internal_fold_left f seed stack
let reduce f stack = internal_fold_left f (hd stack) (tl stack)
Program.fs
open Princess.Collections
let x = Stack.getRange 1 10
printfn "%A" (Stack.hd x)
printfn "%A" (Stack.tl x)
printfn "%A" (Stack.fold ( * ) 1 x)
printfn "%A" (Stack.reduce ( + ) x)
DataStructures
模块在哪里?
您可能预期上面的 Program.fs
中的代码会打开 Princess.Collections.DataStructures
而不是 Princess.Collections
。根据 F# 规范,F# 通过将所有代码放入与代码文件名匹配的隐式模块中来处理匿名实现文件(没有前导 module
或 namespace
声明的文件)。由于我们有一个前导 namespace
声明,F# 不会创建隐式模块。
.NET 不允许用户在类或模块之外创建函数或值。因此,我们无法编写以下代码
namespace Princess.Collections
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let somefunction() = 12 (* <--- functions not allowed outside modules *)
(* ... *)
如果我们更倾向于有一个名为 DataStructures
的模块,我们可以这样写
namespace Princess.Collections
module DataStructures
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let somefunction() = 12
(* ... *)
或者等效地,我们同时定义一个模块并将它放在命名空间中,使用
module Princess.Collections.DataStructures
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let somefunction() = 12
(* ... *)
与模块和类不同,任何文件都可以为命名空间做出贡献。例如
DataStructures.fs
namespace Princess.Collections
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
module Stack =
(* helper functions *)
let internal_head_tail = function
| EmptyStack -> failwith "Empty stack"
| StackNode(hd, tail) -> hd, tail
let rec internal_fold_left f acc = function
| EmptyStack -> acc
| StackNode(hd, tail) -> internal_fold_left f (f acc hd) tail
(* public functions *)
let rec getRange startNum endNum =
if startNum > endNum then EmptyStack
else StackNode(startNum, getRange (startNum+1) endNum)
let hd s = internal_head_tail s |> fst
let tl s = internal_head_tail s |> snd
let fold f seed stack = internal_fold_left f seed stack
let reduce f stack = internal_fold_left f (hd stack) (tl stack)
MoreDataStructures.fs
namespace Princess.Collections
type 'a tree when 'a :> System.IComparable<'a> =
| EmptyTree
| TreeNode of 'a * 'a tree * 'a tree
module Tree =
let rec insert (x : #System.IComparable<'a>) = function
| EmptyTree -> TreeNode(x, EmptyTree, EmptyTree)
| TreeNode(y, l, r) as node ->
match x.CompareTo(y) with
| 0 -> node
| 1 -> TreeNode(y, l, insert x r)
| -1 -> TreeNode(y, insert x l, r)
| _ -> failwith "CompareTo returned illegal value"
由于我们在两个文件中都有一个前导命名空间声明,因此 F# 不会创建任何隐式模块。'a stack
、'a tree
、Stack
和 Tree
类型都可通过 Princess.Collections
命名空间访问
Program.fs
open Princess.Collections
let x = Stack.getRange 1 10
let y =
let rnd = new System.Random()
[ for a in 1 .. 10 -> rnd.Next(0, 100) ]
|> Seq.fold (fun acc x -> Tree.insert x acc) EmptyTree
printfn "%A" (Stack.hd x)
printfn "%A" (Stack.tl x)
printfn "%A" (Stack.fold ( * ) 1 x)
printfn "%A" (Stack.reduce ( + ) x)
printfn "%A" y
与模块不同,命名空间没有等效的签名文件。相反,类和子模块的可见性通过标准的访问修饰符 控制
namespace Princess.Collections
type 'a tree when 'a :> System.IComparable<'a> =
| EmptyTree
| TreeNode of 'a * 'a tree * 'a tree
(* InvisibleModule is only accessible by classes or
modules inside the Princess.Collections namespace*)
module private InvisibleModule =
let msg = "I'm invisible!"
module Tree =
(* InvisibleClass is only accessible by methods
inside the Tree module *)
type private InvisibleClass() =
member x.Msg() = "I'm invisible too!"
let rec insert (x : #System.IComparable<'a>) = function
| EmptyTree -> TreeNode(x, EmptyTree, EmptyTree)
| TreeNode(y, l, r) as node ->
match x.CompareTo(y) with
| 0 -> node
| 1 -> TreeNode(y, l, insert x r)
| -1 -> TreeNode(y, insert x l, r)
| _ -> failwith "CompareTo returned illegal value"