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/iterimap/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"