跳转到内容

F# 编程/模块和命名空间

维基教科书,自由的教学读物
上一页: 事件 索引 下一页: 量纲
F# : 模块和命名空间

模块和命名空间主要用于代码的组织和分组。

定义模块

[编辑 | 编辑源代码]

不需要任何代码来定义模块。如果一个代码文件不包含前导 namespacemodule 声明,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 有许多有用的方法,但假设我们认为它缺少几个重要的函数,ReverseIsPalindrome。由于该类被标记为 sealedNotInheritable,因此我们无法创建该类的派生版本。相反,我们创建一个包含我们想要的新方法的模块。以下是在 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# 通过将所有代码放入与代码文件名匹配的隐式模块中来处理匿名实现文件(没有前导 modulenamespace 声明的文件)。由于我们有一个前导 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 treeStackTree 类型都可通过 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"
上一页: 事件 索引 下一页: 量纲
华夏公益教科书