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