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"
F# 有兩個內建函式,fst 和 snd,它們返回 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.
從 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 -> coords。with 關鍵字建立 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