跳轉到內容

F# 程式設計/更高階函式

來自 Wikibooks,開放書籍,開放世界
前一頁:遞迴 索引 下一頁:可選型別
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 函式

[編輯 | 編輯原始碼]

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 arraytuplestring 等。

還存在 >> 運算子,它也執行函式組合,但順序相反。它定義如下

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 的引數實際上是由匿名函式應用和計算的。

前一頁:遞迴 索引 下一頁:可選型別
華夏公益教科書