跳转到内容

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 的参数实际上是由匿名函数应用和计算的。

前一页:递归 索引 下一页:可选类型
华夏公益教科书