跳轉到內容

F# 程式設計/元組和記錄

來自華夏公益教科書,面向開放世界的開放書籍
前一個:選項型別 索引 下一個:列表
F# : 元組和記錄

定義元組

[編輯 | 編輯原始碼]

元組被定義為一個逗號分隔的值集合。例如,(10, "hello") 是一個 2 元組,型別為 (int * string)。元組對於建立將相關值組合在一起的臨時資料結構非常有用。請注意,括號不是元組的一部分,但通常需要新增它們以確保元組只包含你認為它包含的內容。

let average (a, b) =
    (a + b) / 2.0

此函式的型別為 float * float -> float,它接收一個 float * float 元組並返回另一個 float

> let average (a, b) =
    let sum = a + b
    sum / 2.0;;

val average : float * float -> float

> average (10.0, 20.0);;
val it : float = 15.0

注意,元組被視為單個引數。因此,元組可用於返回多個值

示例 1 - 一個函式,它將一個 3 元組乘以一個標量值以返回另一個 3 元組。

> let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

> scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)

示例 2 - 一個函式,它反轉傳遞給函式的任何內容的輸入。

> let swap (a, b) = (b, a);;
val swap : 'a * 'b -> 'b * 'a

> swap ("Web", 2.0);;
val it : float * string = (2.0, "Web")

> swap (20, 30);;
val it : int * int = (30, 20)

示例 3 - 一個函式,它除兩個數並同時返回餘數。

> let divrem x y =
    match y with
    | 0 -> None
    | _ -> Some(x / y, x % y);;

val divrem : int -> int -> (int * int) option

> divrem 100 20;; (* 100 / 20 = 5 remainder 0 *)
val it : (int * int) option = Some (5, 0)

> divrem 6 4;; (* 6 / 4 = 1 remainder 2 *)
val it : (int * int) option = Some (1, 2)

> divrem 7 0;; (* 7 / 0 throws a DivisionByZero exception *)
val it : (int * int) option = None

每個元組都有一個名為 arity 的屬性,它表示用於定義元組的引數數量。例如,一個 int * string 元組由兩部分組成,因此它具有 2 的元數,一個 string * string * float 具有 3 的元數,依此類推。

元組模式匹配

[編輯 | 編輯原始碼]

元組上的模式匹配很簡單,因為用於宣告元組型別的語法也用於匹配元組。

示例 1

假設我們有一個函式 greeting,它根據指定的名字和/或語言打印出自定義問候語。

let greeting (name, language) =
    match (name, language) with
    | ("Steve", _) -> "Howdy, Steve"
    | (name, "English") -> "Hello, " + name
    | (name, _) when language.StartsWith("Span") -> "Hola, " + name
    | (_, "French") -> "Bonjour!"
    | _ -> "DOES NOT COMPUTE"

此函式的型別為 string * string -> string,這意味著它接收一個 2 元組並返回一個字串。我們可以在 fsi 中測試此函式

> greeting ("Steve", "English");;
val it : string = "Howdy, Steve"
> greeting ("Pierre", "French");;
val it : string = "Bonjour!"
> greeting ("Maria", "Spanish");;
val it : string = "Hola, Maria"
> greeting ("Rocko", "Esperanto");;
val it : string = "DOES NOT COMPUTE"

示例 2

我們可以使用替代模式匹配語法方便地匹配元組的形狀

> let getLocation = function
    | (0, 0) -> "origin"
    | (0, y) -> "on the y-axis at y=" + y.ToString()
    | (x, 0) -> "on the x-axis at x=" + x.ToString()
    | (x, y) -> "at x=" + x.ToString() + ", y=" + y.ToString() ;;

val getLocation : int * int -> string

> getLocation (0, 0);;
val it : string = "origin"
> getLocation (0, -1);;
val it : string = "on the y-axis at y=-1"
> getLocation (5, -10);;
val it : string = "at x=5, y=-10"
> getLocation (7, 0);;
val it : string = "on the x-axis at x=7"

fstsnd

[編輯 | 編輯原始碼]

F# 有兩個內建函式,fstsnd,它們返回 2 元組中的第一個和第二個專案。這些函式定義如下

let fst (a, b) = a
let snd (a, b) = b

它們具有以下型別

val fst : 'a * 'b -> 'a
val snd : 'a * 'b -> 'b

以下是在 FSI 中的幾個示例

> fst (1, 10);;
val it : int = 1
> snd (1, 10);;
val it : int = 10
> fst ("hello", "world");;
val it : string = "hello"
> snd ("hello", "world");;
val it : string = "world"
> fst ("Web", 2.0);;
val it : string = "Web"
> snd (50, 100);;
val it : int = 100

同時分配多個變數

[編輯 | 編輯原始碼]

元組可用於同時分配多個值。這與 Python 中的元組解包相同。執行此操作的語法為

let val1, val2, ... valN = (expr1, expr2, ... exprN)

換句話說,你將一個逗號分隔的N個值列表分配給一個N元組。以下是在 FSI 中的示例

> let x, y = (1, 2);;

val y : int
val x : int

> x;;
val it : int = 1

> y;;
val it : int = 2

分配的值數量必須與函式返回的元組的元數匹配,否則 F# 將引發異常

> let x, y = (1, 2, 3);;

  let x, y = (1, 2, 3);;
  ------------^^^^^^^^

stdin(18,13): error FS0001: Type mismatch. Expecting a
	'a * 'b
but given a
	'a * 'b * 'c.
The tuples have differing lengths of 2 and 3.

元組和 .NET 框架

[編輯 | 編輯原始碼]

從 F# 的角度來看,.NET 基本類庫中的所有方法都接收一個引數,該引數是具有不同型別和元數的元組。例如

C# 函式簽名 F# 函式簽名
System.String String Join(String separator, String[] value) val Join : (string * string array) -> string
System.Net.WebClient void DownloadFile(String uri, String fileName) val DownloadFile : (string * string) -> unit
System.Convert String ToString(int value, int toBase) val ToString : (int * int) -> string
System.Math int DivRem(int a, int b, out int remainder) val DivRem : (int * int) -> (int * int)
System.Int32 bool TryParse(String value, out int result) val TryParse : string -> (bool * int)

一些方法,例如上面顯示的 System.Math.DivRem 以及其他方法,例如 System.Int32.TryParse 透過輸出變數返回多個值。F# 允許程式設計師省略輸出變數;使用此呼叫約定,F# 將以元組的形式返回函式的結果,例如

> System.Int32.TryParse("3");;
val it : bool * int = (true, 3)

> System.Math.DivRem(10, 7);;
val it : int * int = (1, 3)

定義記錄

[編輯 | 編輯原始碼]

記錄類似於元組,只是它包含命名欄位。使用以下語法定義記錄

type recordName =
    { [ fieldName : dataType ] + }
+ 表示元素必須出現一次或多次。

以下是一個簡單的記錄

type website =
    { Title : string;
        Url : string }

與元組不同,記錄使用 type 關鍵字顯式定義為它自己的型別,記錄欄位定義為用分號分隔的列表。(在許多方面,記錄可以被認為是一個簡單的。)

透過指定記錄的欄位來建立 website 記錄,如下所示

> let homepage = { Title = "Google"; Url = "http://www.google.com" };;
val homepage : website

請注意,F# 透過欄位的名稱和型別來確定記錄的型別,而不是使用欄位的順序。例如,雖然上面的記錄是使用 Title 首先和 Url 最後定義的,但以下寫法完全合法

> { Url = "http://www.microsoft.com/"; Title = "Microsoft Corporation" };;
val it : website = {Title = "Microsoft Corporation";
                    Url = "http://www.microsoft.com/";}

使用點表示法很容易訪問記錄的屬性

> let homepage = { Title = "Wikibooks"; Url = "http://www.wikibooks.org/" };;

val homepage : website

> homepage.Title;;
val it : string = "Wikibooks"

> homepage.Url;;
val it : string = "http://www.wikibooks.org/"

克隆記錄

[編輯 | 編輯原始碼]

記錄是不可變型別,這意味著不能修改記錄的例項。但是,可以使用克隆語法方便地克隆記錄

type coords = { X : float; Y : float }

let setX item newX =
    { item with X = newX }

方法 setX 的型別為 coords -> float -> coordswith 關鍵字建立 item 的一個副本,並將它的 X 屬性設定為 newX

> let start = { X = 1.0; Y = 2.0 };;
val start : coords

> let finish = setX start 15.5;;
val finish : coords

> start;;
val it : coords = {X = 1.0;
                   Y = 2.0;}
> finish;;
val it : coords = {X = 15.5;
                   Y = 2.0;}

請注意,setX 建立了記錄的副本,它實際上並沒有改變原始記錄例項。

以下是一個更完整的程式

type TransactionItem =
    { Name : string;
        ID : int;
        ProcessedText : string;
        IsProcessed : bool }

let getItem name id =
    { Name = name; ID = id; ProcessedText = null; IsProcessed = false }

let processItem item =
    { item with
        ProcessedText = "Done";
        IsProcessed = true }
    
let printItem msg item =
    printfn "%s: %A" msg item

let main() =
    let preProcessedItem = getItem "Steve" 5
    let postProcessedItem = processItem preProcessedItem

    printItem "preProcessed" preProcessedItem
    printItem "postProcessed" postProcessedItem
    
main()

此程式處理 TransactionItem 類的例項並列印結果。此程式輸出以下內容

preProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = null;
 IsProcessed = false;}
postProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = "Done";
 IsProcessed = true;}

記錄模式匹配

[編輯 | 編輯原始碼]

我們可以像匹配元組一樣輕鬆地匹配記錄

open System

type coords = { X : float; Y : float }
 
let getQuadrant = function
    | { X = 0.0; Y = 0.0 } -> "Origin"
    | item when item.X >= 0.0 && item.Y >= 0.0 -> "I"
    | item when item.X <= 0.0 && item.Y >= 0.0 -> "II"
    | item when item.X <= 0.0 && item.Y <= 0.0 -> "III"
    | item when item.X >= 0.0 && item.Y <= 0.0 -> "IV"
 
let testCoords (x, y) =
    let item = { X = x; Y = y }
    printfn "(%f, %f) is in quadrant %s" x y (getQuadrant item)
 
let main() =
    testCoords(0.0, 0.0)
    testCoords(1.0, 1.0)
    testCoords(-1.0, 1.0)
    testCoords(-1.0, -1.0)
    testCoords(1.0, -1.0)
    Console.ReadKey(true) |> ignore
 
main()

請注意,模式情況使用與建立記錄相同的語法定義(如第一個情況所示),或者使用保護條件(如其餘情況所示)。不幸的是,程式設計師不能在模式情況中使用克隆語法,因此像 | { item with X = 0 } -> "y-axis" 這樣的情況將無法編譯。

上面的程式輸出

(0.000000, 0.000000) is in quadrant Origin
(1.000000, 1.000000) is in quadrant I
(-1.000000, 1.000000) is in quadrant II
(-1.000000, -1.000000) is in quadrant III
(1.000000, -1.000000) is in quadrant IV
前一個:選項型別 索引 下一個:列表
華夏公益教科書