F# 之旅(上)
寫在前面的話
解答一下在上一篇文章《在Visual Studio中入門F#》中有人的提問,
1. 問:是準備寫 F# 系列嗎?
答:當然不是,本人也是剛剛學習 F#,只是翻譯微軟官方的文檔,但是我會盡力翻譯更多的文章。
2. 問:你們的項目使用F#寫的嗎?
答:本人大三學生,也不是什麽大佬,興趣而已。
在這篇文章中
- 怎樣運行示例代碼
- 函數和模塊
- 數字、布爾值和字符串
- 元組
- 管線和組成
- 列表、數組和序列
學習 F# 最好的方式是讀寫 F# 代碼。本文將介紹 F# 語言的一些主要功能,並給您一些可以在您計算機上執行的代碼片段。
F# 中有兩個重要概念:函數和類型。本教程將強調 F# 的這兩個特點。
原文鏈接
Tour of F#
備註,原文較長,本人分為《F# 之旅》上下兩部分翻譯。
怎樣運行示例代碼
執行這些示例代碼最快的方式是使用 F# Interactive。僅僅通過拷貝/粘貼示例代碼就能運行它們了。當然,您也可以在 Visual Studio 中建立一個控制臺應用程序來編譯並運行它們。
函數或模塊
組織在模塊中的函數是任何 F# 程序最基本的部分。函數執行根據輸入以生成輸出的工作,並使用模塊進行組織,這是在 F# 中組織事物的主要方式。函數使用“let 綁定”關鍵字定義, 需要一個名稱並定義參數。
1 module BasicFunctions = 2 3 ///您使用“let”定義一個函數。這個函數接收一個整數類型的參數,返回一個整數。 4 let sampleFunction1 x = x*x + 3 5 6 /// 使用函數,使用“let”命名函數的返回值。 7 /// 變量的類型由函數的返回值推斷而出。 8 let result1 = sampleFunction1 4573 9 10 // 本行使用“%d”以 int 類型格式輸出結果。這是類型安全的。 11 // 如果“result1”不是“int”類型,那麽該行將無法編譯成功。 12 printfn "The result of squaring the integer 4573 and adding 3 is %d" result1 13 14 /// 當有需要時,可使用“(argument:type)”註明參數的類型。括號是必需的。 15 let sampleFunction2 (x:int) = 2*x*x - x/5 + 3 16 17 let result2 = sampleFunction2 (7 + 4) 18 printfn "The result of applying the 2nd sample function to (7 + 4) is %d" result2 19 20 /// 條件表達式由 if/then/elif/else 構成。 21 /// 22 /// 註意 F# 使用空格縮進語法,與 Python 相似。 23 let sampleFunction3 x = 24 if x < 100.0 then 25 2.0*x*x - x/5.0 + 3.0 26 else 27 2.0*x*x + x/5.0 - 37.0 28 29 let result3 = sampleFunction3 (6.5 + 4.5) 30 31 // 本行使用“%f”將結果以 float 類型格式輸出。同上述的“%d”,這是類型安全的。 32 printfn "The result of applying the 2nd sample function to (6.5 + 4.5) is %f" result3
“let 綁定”關鍵字同樣可以將值綁定到名稱上,與其他語言中的變量類似。默認情況下,綁定後便不可變更,這意味值或函數綁定到名稱上後,就不能在更改了。這與其他語言中的變量不同,它們是可變的,也就是說,它們的值可以在任何時間點上更改。如果需要可變的綁定,可以使用“let mutable”語法。
1 module Immutability = 2 3 /// 使用“let”將一個值綁定到名稱上,它不可改變。 4 /// 5 /// 代碼第二行編譯失敗,因為“number”是不可變的。 6 /// 重定義“number”為一個不同的值,在 F# 中是不允許的。 7 let number = 2 8 // let number = 3 9 10 /// 一個可變的綁定。“otherNumber”能夠改變值。 11 let mutable otherNumber = 2 12 13 printfn "‘otherNumber‘ is %d" otherNumber 14 15 // 當需要改變值時,使用“<-”分配一個新值。 16 // 17 // 註意,“=”與此不同,“=”用於判斷相等。 18 otherNumber <- otherNumber + 1 19 20 printfn "‘otherNumber‘ changed to be %d" otherNumber
數字、布爾值和字符串
作為一門 .NET 語言,F# 同樣支持 .NET 底層的基本類型。
下面是在 F# 中表示各種數值類型的方式:
1 module IntegersAndNumbers = 2 3 /// 這是整數示例。 4 let sampleInteger = 176 5 6 /// 這是浮點數示例。 7 let sampleDouble = 4.1 8 9 /// 這裏使用一些運算符計算得到一個新的數字。數字類型使用 10 /// “int”,“double”等函數進行轉換。 11 let sampleInteger2 = (sampleInteger/4 + 5 - 7) * 4 + int sampleDouble 12 13 /// 這是一個從0到99的數字列表。 14 let sampleNumbers = [ 0 .. 99 ] 15 16 /// 這是一個由0到99的數字和它們的平方數構成的元組所形成的列表。 17 let sampleTableOfSquares = [ for i in 0 .. 99 -> (i, i*i) ] 18 19 // 接下來的一行輸出這個包含許多元組的列表,使用“%A”進行泛型化輸出。 20 printfn "The table of squares from 0 to 99 is:\n%A" sampleTableOfSquares
布爾值像這樣執行基礎的條件判斷邏輯:
1 module Booleans = 2 3 /// 布爾類型的值為“true”和“fales”。 4 let boolean1 = true 5 let boolean2 = false 6 7 /// 對布爾類型進行操作的運算符有“not”,“&&”和“||”。 8 let boolean3 = not boolean1 && (boolean2 || false) 9 10 // 本行使用“%d”輸出布爾類型值。這是類型安全的。 11 printfn "The expression ‘not boolean1 && (boolean2 || false)‘ is %b" boolean3
下面介紹基本的字符串操作:
1 module StringManipulation = 2 3 /// 字符串需要使用雙引號。 4 let string1 = "Hello" 5 let string2 = "world" 6 7 /// [email protected] 8 /// 這將忽略“\”,“\n”,“\t”等轉義字符。 9 let string3 = @"C:\Program Files\" 10 11 /// 字符串文本也可以使用三重引號。 12 let string4 = """The computer said "hello world" when I told it to!""" 13 14 /// 字符串通常使用“+”運算符進行連接。 15 let helloWorld = string1 + " " + string2 16 17 // 本行使用“%d”輸出一個字符串變量。這是類型安全的。 18 printfn "%s" helloWorld 19 20 /// 子字符串使用索引器表示。本行提取前7個字符作為子字符串。 21 /// 註意,與許多編程語言相同,字符串在 F# 中從0開始索引。 22 printfn "%s" substring
元組
元組在 F# 中處於很重要的位置。它是一組未命名的,但有序的值,它們整體就能當作一個值。可以將它們理解為由其他值聚合而成的值。它們有許多的用途,例如方便函數返回多個值,方便特殊值組織在一起。
1 module Tuples = 2 3 /// 一個由整數構成的元組示例。tuple1 的類型是 int*int*int 4 let tuple1 = (1, 2, 3) 5 6 /// 一個交換元組中兩個值順序的函數。 7 /// 8 /// F# 類型推斷將自動泛化函數,意味著可以在任何類型下工作。 9 let swapElems (a, b) = (b, a) 10 11 printfn "The result of swapping (1, 2) is %A" (swapElems (1,2)) 12 13 /// 一個由一個整數、一個字符串和一個雙精度浮點數組成的元組。tuple2 的類型是 int*string*float 14 let tuple2 = (1, "fred", 3.1415) 15 16 printfn "tuple1: %A\ttuple2: %A" tuple1 tuple2
在 F# 4.1 中,您還可以使用 struct 關鍵字將一個元組定義為結構體元組。這些同樣可以與C# 7/Visual Basic 15 中的結構體元組進行互操作:
1 /// 元組通常是對象,但是它們也可以表示為結構體。 2 /// 3 /// 它們完全可以與 C# 和 Visual Basic.NET 中的結構體元組進行互操作。 4 /// 結構體元組不能隱式轉換為對象元組 (通常稱為引用元組)。 5 /// 6 /// 因為上述原因,下面的第二行將無法編譯。 7 let sampleStructTuple = struct (1, 2) 8 //let thisWillNotCompile: (int*int) = struct (1, 2) 9 10 // 您可以這樣做。 11 let convertFromStructTuple (struct(a, b)) = (a, b) 12 let convertToStructTuple (a, b) = struct(a, b) 13 14 printfn "Struct Tuple: %A\nReference tuple made from the Struct Tuple: %A" sampleStructTuple (sampleStructTuple |> convertFromStructTuple)
您需要特別註意,因為結構體元組是值類型,您不能將它們隱式轉換為引用元組,反之亦然。引用元組和結構體元組間必需進行顯示轉換。
管線和組成
管道運算符(| >, <|, | |> |>, <| | |) 和組合運算符 (>>
和 <<
) 在 F# 中被廣泛用於數據處理。這些運算符是函數,它們允許您以靈活的方式來創建函數的“管線”。下面的示例將帶您瀏覽如何利用這些運算符來構建一個簡單的功能管線。
1 module PipelinesAndComposition = 2 3 /// 計算 x 的平方。 4 let square x = x * x 5 6 /// 計算 x + 1。 7 let addOne x = x + 1 8 9 /// 測試 x 是否是奇數。 10 let isOdd x = x % 2 <> 0 11 12 /// 一個包含5個數字的列表。 13 let numbers = [ 1; 2; 3; 4; 5 ] 14 15 /// 傳入一個數字列表,它將篩選出偶數, 16 /// 再計算結果的平方,然後加1。 17 let squareOddValuesAndAddOne values = 18 let odds = List.filter isOdd values 19 let squares = List.map square odds 20 let result = List.map addOne squares 21 result 22 23 printfn "processing %A through ‘squareOddValuesAndAddOne‘ produces: %A" numbers (squareOddValuesAndAddOne numbers) 24 25 /// 修改 “squareOddValuesAndAddOne” 更短的方法是將每個過程中產生的中間 26 /// 結果嵌套到函數調用本身。 27 /// 28 /// 這樣使函數變得更短,但是這樣很難查看函數的執行順序。 29 let squareOddValuesAndAddOneNested values = 30 List.map addOne (List.map square (List.filter isOdd values)) 31 32 printfn "processing %A through ‘squareOddValuesAndAddOneNested‘ produces: %A" numbers (squareOddValuesAndAddOneNested numbers) 33 34 /// 一個更好的方式去編寫 “squareOddValuesAndAddOne” 函數,那就是使用 F# 35 /// 的管道運算符。這與函數嵌套一樣允許您避免創建中間結果,但仍保持較高的可讀性。 36 let squareOddValuesAndAddOnePipeline values = 37 values 38 |> List.filter isOdd 39 |> List.map square 40 |> List.map addOne 41 42 printfn "processing %A through ‘squareOddValuesAndAddOnePipeline‘ produces: %A" numbers (squareOddValuesAndAddOnePipeline numbers) 43 44 /// 您可以繼續精簡 “squareOddValuesAndAddOnePipeline”函數,通過使用 45 /// Lamdba表達式移除第二個 “List.map” 46 /// 47 /// 註意,Lamdba表達式中也同樣用到管道運算符。 48 /// can be used for single values as well. This makes them very powerful for processing data. 49 let squareOddValuesAndAddOneShorterPipeline values = 50 values 51 |> List.filter isOdd 52 |> List.map(fun x -> x |> square |> addOne) 53 54 printfn "processing %A through ‘squareOddValuesAndAddOneShorterPipeline‘ produces: %A" numbers (squareOddValuesAndAddOneShorterPipeline numbers) 55 56 /// 最後,您可以解決需要顯示地采用值作為參數的問題,通過使用“>>”來編寫兩個核 57 /// 心操作:篩選出偶數,然後平方和加1。同樣,Lamdba表達式“fun x-> ...”也不需 58 /// 要,因為x 只是在該範圍中被定義,以便將其傳入函數管線。因此,“>>”可以在這 59 /// 裏使用。 60 /// 61 /// “squareOddValuesAndAddOneComposition”的結果本身就是一個將整數列表作 62 /// 為其輸入的函數。 如果您使用整數列表執行“squareOddValuesAndAddOneComposition”, 63 /// 則會註意到它與以前的函數相同。 64 /// 65 /// 這是使用所謂的函數組合。 這是可能的,因為F#中的函數使用Partial Application, 66 ///每個數據處理操作的輸入和輸出類型與我們使用的函數的簽名相匹配。 67 let squareOddValuesAndAddOneComposition = 68 List.filter isOdd >> List.map (square >> addOne) 69 70 printfn "processing %A through ‘squareOddValuesAndAddOneComposition‘ produces: %A" numbers (squareOddValuesAndAddOneComposition numbers)
上述的示例使用了許多 F# 的特性,包括列表處理函數,頭等函數和部分應用程序。雖然對於每個概念都有深刻的理解是較為困難的,但應該清楚的是,在使用函數管線來處理數據有多麽容易。
列表、數組和序列
列表、數組和序列是 F# 核心庫中3個基礎的集合類型。
列表是有序的、不可變的、具有相同類型元素的集合。它們是單鏈表,這意味著它們是易於枚舉的,但是如果它們很大,則不易於隨機存取和則隨機訪問和級聯。這與其他流行語言中的列表不同,後者通常不使用單鏈表來表示列表。
1 module Lists = 2 3 /// 列使用“[...]”定義,這是一個空列表。 4 let list1 = [ ] 5 6 /// 這是一個包含3個元素的列表, “;”用於分割在同一行的元素。 7 let list2 = [ 1; 2; 3 ] 8 9 /// 您也可以將各元素獨占一行以進行分割。 10 let list3 = [ 11 1 12 2 13 3 14 ] 15 16 /// 這是一個包含1到1000整數的列表。 17 let numberList = [ 1 .. 1000 ] 18 19 /// 列表可以通過計算得到,這是包含一年中所有天的列表。 20 let daysList = 21 [ for month in 1 .. 12 do 22 for day in 1 .. System.DateTime.DaysInMonth(2017, month) do 23 yield System.DateTime(2017, month, day) ] 24 25 // 使用“List.take”輸出“dayList”中的前5個元素。 26 printfn "The first 5 days of 2017 are: %A" (daysList |> List.take 5) 27 28 /// 計算中可以包含條件判斷。 這是一個包含棋盤上的黑色方塊的坐標元組的列表。 29 let blackSquares = 30 [ for i in 0 .. 7 do 31 for j in 0 .. 7 do 32 if (i+j) % 2 = 1 then 33 yield (i, j) ] 34 35 /// 列表可以使用“List.map”和其他函數式編程組合器進行轉換。 此處通過使用 36 /// 使用管道運算符將參數傳遞給List.map,計算列表中數字的平方,產生一個新的列表。 37 let squares = 38 numberList 39 |> List.map (fun x -> x*x) 40 41 /// 還有很多其他列表組合器。如下計算能被3整除的數字的平方數。 42 let sumOfSquares = 43 numberList 44 |> List.filter (fun x -> x % 3 = 0) 45 |> List.sumBy (fun x -> x * x) 46 47 printfn "The sum of the squares of numbers up to 1000 that are divisible by 3 is: %d" sumOfSquares
數組是大小固定的、可變的、具有相同類型的元素的集合。 它們支持元素的快速隨機訪問,並且比F#列表更快,因為它們是連續的內存塊。
1 module Arrays = 2 3 /// 這是一個空數組。註意,語法與列表相似,但是數組使用的是“[| ... |]”。 4 let array1 = [| |] 5 6 /// 數組使用與列表相同的方式分割元素。 7 let array2 = [| "hello"; "world"; "and"; "hello"; "world"; "again" |] 8 9 /// 這是一個包含1到1000整數的數組。 10 let array3 = [| 1 .. 1000 |] 11 12 /// 這是一個只包含“hello”和“world”的數組。 13 let array4 = 14 [| for word in array2 do 15 if word.Contains("l") then 16 yield word |] 17 18 /// 這是一個由索引初始化的數組,其中包含從0到2000的偶數。 19 let evenNumbers = Array.init 1001 (fun n -> n * 2) 20 21 /// 使用切片符號提取子數組。 22 let evenNumbersSlice = evenNumbers.[0..500] 23 24 /// 您可以使用“for”遍歷數組和列表。 25 for word in array4 do 26 printfn "word: %s" word 27 28 // 您可以使用左箭頭分配運算符修改數組元素的內容。 29 array2.[1] <- "WORLD!" 30 31 /// 您可以使用“Array.map”和其他函數式編程操作來轉換數組。 32 /// 以下計算以“h”開頭的單詞的長度之和。 33 let sumOfLengthsOfWords = 34 array2 35 |> Array.filter (fun x -> x.StartsWith "h") 36 |> Array.sumBy (fun x -> x.Length) 37 38 printfn "The sum of the lengths of the words in Array 2 is: %d" sumOfLengthsOfWords
序列是一系列邏輯的元素,全部是相同的類型。 這些是比列表和數組更常用的類型,可以將其作為任何邏輯元素的“視圖”。 它們脫穎而出,因為它們可以是惰性的,這意味著元素只有在需要時才被計算出來。
1 module Sequences = 2 3 /// 這是一個空的隊列。 4 let seq1 = Seq.empty 5 6 /// 這是這是含有值的隊列。 7 let seq2 = seq { yield "hello"; yield "world"; yield "and"; yield "hello"; yield "world"; yield "again" } 8 9 /// 這是包含1到1000整數的隊列。 10 let numbersSeq = seq { 1 .. 1000 } 11 12 /// 這是包含“hello”和“world”的隊列。 13 let seq3 = 14 seq { for word in seq2 do 15 if word.Contains("l") then 16 yield word } 17 18 /// 這個隊列包含到2000範圍內的偶數。 19 let evenNumbers = Seq.init 1001 (fun n -> n * 2) 20 21 let rnd = System.Random() 22 23 /// 這是一個隨機的無限序列。 24 /// 這個例子使用“yield!”返回子隊列中的每個元素。 25 let rec randomWalk x = 26 seq { yield x 27 yield! randomWalk (x + rnd.NextDouble() - 0.5) } 28 29 /// 這個例子顯示了隨機產生的前100個元素。 30 let first100ValuesOfRandomWalk = 31 randomWalk 5.0 32 |> Seq.truncate 100 33 |> Seq.toList 34 35 printfn "First 100 elements of a random walk: %A" first100ValuesOfRandomWalk
F# 之旅(上)