F# 程式設計/更高階函式
| F# : 高階函式 |
一個更高階函式是一個函式,它以另一個函式作為引數,或者一個函式,它返回另一個函式作為值,或者一個函式,它同時做這兩件事。
為了更好地理解更高階函式,如果你曾經學習過微積分的入門課程,你一定熟悉兩個函式:極限函式和導數函式。
極限函式定義如下
極限函式 lim 以另一個函式 f(x) 作為引數,並返回一個值 L 來表示極限。
類似地,導數函式定義如下
導數函式 deriv 以函式 f(x) 作為引數,並返回一個完全不同的函式 f'(x) 作為結果。
在這方面,我們可以正確地假設極限和導數函式是更高階函式。如果我們對數學中的更高階函式有很好的理解,那麼我們可以在 F# 程式碼中應用相同的原理。
在 F# 中,我們可以將一個函式傳遞給另一個函式,就像它是一個字面量值一樣,並像呼叫任何其他函式一樣呼叫它。例如,這裡有一個非常簡單的函式
let passFive f = (f 5)
在 F# 符號中,passFive 有以下型別
val passFive : (int -> 'a) -> 'a
換句話說,passFive 接收一個函式 f,其中 f 必須接收一個 int 並返回任何泛型型別 'a。我們的函式 passFive 的返回型別是 'a,因為我們事先不知道 f 5 的返回型別。
open System
let square x = x * x
let cube x = x * x * x
let sign x =
if x > 0 then "positive"
else if x < 0 then "negative"
else "zero"
let passFive f = (f 5)
printfn "%A" (passFive square) // 25
printfn "%A" (passFive cube) // 125
printfn "%A" (passFive sign) // "positive"
這些函式有以下型別
val square : int -> int
val cube : int -> int
val sign : int -> string
val passFive : (int -> 'a) -> 'a
與許多其他語言不同,F# 並沒有區分函式和值。我們以與傳遞整數、字串和其他值完全相同的方式將函式傳遞給其他函式。
Map 函式將一種型別的資料轉換為另一種型別的資料。F# 中一個簡單的 Map 函式如下所示
let map item converter = converter item
它的型別是 val map : 'a -> ('a -> 'b) -> 'b。換句話說,map 接收兩個引數:一個專案 'a,以及一個接收一個 'a 並返回一個 'b 的函式;map 返回一個 'b。
讓我們檢查以下程式碼
open System
let map x f = f x
let square x = x * x
let cubeAndConvertToString x =
let temp = x * x * x
temp.ToString()
let answer x =
if x = true then "yes"
else "no"
let first = map 5 square
let second = map 5 cubeAndConvertToString
let third = map true answer
這些函式有以下簽名
val map : 'a -> ('a -> 'b) -> 'b
val square : int -> int
val cubeAndConvertToString : int -> string
val answer : bool -> string
val first : int
val second : string
val third : string
first 函式傳遞一個數據型別 int 和一個具有簽名 (int -> int) 的函式;這意味著 map 函式中的佔位符 'a 和 'b 都變成了 int。
second 函式傳遞一個數據型別 int 和一個函式 (int -> string),並且 map 預計會返回一個 string。
third 函式傳遞一個數據型別 bool 和一個函式 (bool -> string),並且 map 返回一個 string,正如我們所期望的那樣。
由於我們的泛型程式碼是型別安全的,如果我們寫下以下程式碼,就會得到一個錯誤
let fourth = map true square
因為 true 將我們的函式約束為型別 (bool -> 'b),但 square 函式的型別是 (int -> int),所以顯然不正確。
在代數中,組合函式定義為 compose(f, g, x) = f(g(x)),表示為 f o g。在 F# 中,組合函式定義如下
let inline (<<) f g x = f (g x)
它的簽名有些繁瑣:val << : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b。
如果我有兩個函式
- f(x) = x^2
- g(x) = -x/2 + 5
並且我想模擬 f o g,我可以寫
open System
let f x = x*x
let g x = -x/2.0 + 5.0
let fog = f << g
Console.WriteLine(fog 0.0) // 25
Console.WriteLine(fog 1.0) // 20.25
Console.WriteLine(fog 2.0) // 16
Console.WriteLine(fog 3.0) // 12.25
Console.WriteLine(fog 4.0) // 9
Console.WriteLine(fog 5.0) // 6.25
請注意 fog 不會返回一個值,它返回一個簽名為 (float -> float) 的另一個函式。
組合函式沒有理由必須侷限於數字。由於它具有泛型性,它可以與任何資料型別一起使用,例如 int array、tuple、string 等。
還存在 >> 運算子,它也執行函式組合,但順序相反。它定義如下
let inline (>>) f g x = g (f x)
此運算子的簽名如下:val >> : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c。
使用 >> 運算子進行組合的優點是,組合中的函式按呼叫順序排列。
let gof = f >> g
這首先應用 f,然後在結果上應用 g。
管道前向運算子 |> 是 F# 中最重要的運算子之一。管道前向運算子的定義非常簡單
let inline (|>) x f = f x
讓我們取 3 個函式
let square x = x * x
let add x y = x + y
let toString x = x.ToString()
假設我們有一個複雜的函式,它對一個數字進行平方,然後加上 5,最後將其轉換為字串?通常,我們會這樣寫
let complexFunction x =
toString (add 5 (square x))
我們可以使用管道前向運算子來改善此函式的可讀性
let complexFunction x =
x |> square |> add 5 |> toString
x 被管道傳遞到 square 函式,然後管道傳遞到 add 5 方法,最後管道傳遞到 toString 方法。
到目前為止,本書中展示的所有函式都是有名稱的。例如,上面的函式名為 add。F# 允許程式設計師使用 fun 關鍵字宣告無名函式,即匿名函式。
let complexFunction =
2 (* 2 *)
|> ( fun x -> x * x) (* 2 * 2 = 4 *)
|> ( fun x -> x + 5) (* 4 + 5 = 9 *)
|> ( fun x -> x.ToString() ) (* 9.ToString = "9" *)
匿名函式很方便,在很多地方都有用。
open System
let duration f =
let timer = new System.Diagnostics.Stopwatch()
timer.Start()
let returnValue = f()
printfn "Elapsed Time: %i" timer.ElapsedMilliseconds
returnValue
let rec fib = function
| 0 -> 0
| 1 -> 1
| n -> fib (n - 1) + fib (n - 2)
let main() =
printfn "fib 5: %i" (duration ( fun() -> fib 5 ))
printfn "fib 30: %i" (duration ( fun() -> fib 30 ))
main()
duration 函式的型別是 val duration : (unit -> 'a) -> 'a。該程式列印
Elapsed Time: 1 fib 5: 5 Elapsed Time: 5 fib 30: 832040
- 注意:執行這些函式的實際持續時間因機器而異。
F# 中一個引人入勝的功能叫做 "柯里化",這意味著 F# 不要求程式設計師在呼叫函式時提供所有引數。例如,假設我們有一個函式
let add x y = x + y
add 接受兩個整數並返回另一個整數。在 F# 表示法中,這寫成 val add : int -> int -> int
我們可以定義另一個函式,如下所示
let addFive = add 5
addFive 函式呼叫了 add 函式,其中一個引數是 add 函式的引數,那麼這個函式的返回值是什麼呢?很簡單:addFive 返回另一個函式,它等待著剩下的引數。在本例中,addFive 返回一個接受 int 並返回另一個 int 的函式,在 F# 表示法中表示為 val addFive : (int -> int)。
您可以像呼叫其他函式一樣呼叫 addFive
open System
let add x y = x + y
let addFive = add 5
Console.WriteLine(addFive 12) // prints 17
函式 let add x y = x + y 的型別為 val add : int -> int -> int。F# 使用稍微非傳統的箭頭表示法來表示函式簽名是有原因的:箭頭表示法與柯里化和匿名函式本質上是相關的。柯里化之所以有效,是因為在幕後,F# 將函式引數轉換為看起來像這樣的樣式
let add = (fun x -> (fun y -> x + y) )
型別 int -> int -> int 在語義上等同於 (int -> (int -> int))。
當您不帶任何引數呼叫 add 時,它將返回 fun x -> fun y -> x + y(或等效地 fun x y -> x + y),另一個等待剩餘引數的函式。同樣地,當您向上面的函式提供一個引數,比如 5,它將返回 fun y -> 5 + y,另一個等待剩餘引數的函式,其中所有 x 的出現都被引數 5 替換了。
柯里化基於這樣一個原則,即每個引數實際上都返回一個單獨的函式,這就是為什麼只用部分引數呼叫函式會返回另一個函式的原因。我們到目前為止看到的熟悉的 F# 語法,let add x y = x + y,實際上是上面顯示的顯式柯里化風格的一種語法糖。
您可能想知道為什麼有兩種模式匹配語法
| 傳統語法 | 快捷語法 |
|---|---|
let getPrice food =
match food with
| "banana" -> 0.79
| "watermelon" -> 3.49
| "tofu" -> 1.09
| _ -> nan
|
let getPrice2 = function
| "banana" -> 0.79
| "watermelon" -> 3.49
| "tofu" -> 1.09
| _ -> nan
|
這兩段程式碼是相同的,但是為什麼快捷語法允許程式設計師在函式定義中省略 food 引數呢?答案與柯里化有關:在幕後,F# 編譯器將 function 關鍵字轉換為以下結構
let getPrice2 =
(fun x ->
match x with
| "banana" -> 0.79
| "watermelon" -> 3.49
| "tofu" -> 1.09
| _ -> nan)
換句話說,F# 將 function 關鍵字視為一個接受一個引數並返回一個值的匿名函式。getPrice2 函式實際上返回一個匿名函式;傳遞給 getPrice2 的引數實際上是由匿名函式應用和計算的。