F#奇妙游(4):F#的函数

let关键词

如果熟悉其他语言,你可能会认为let关键词是用来声明变量的。但在F#中,let关键词是用来声明值的。值是不可变的,而变量是可变的。(注:在F#中,变量是用关键词mutable来声明的。)值的概念,是函数式编程中的一个重要概念。值和变量的区别,是函数式编程和命令式编程的一个重要区别。变量,本质上是内存中一个特定长度的存储区域,用来存储数据。而值,可以理解为是一个表达式,用来计算数据,函数式编程中并不关心其对应的地址,只使用值代表函数的输入或者输出。

let x = 1
let f x = x * x

上面的例子中,x是一个值,而不是一个变量。x的值是1。f是一个函数,它的输入是x,输出是x * x。

从这里,可以看到两个概念:

  1. 不可变的值;
  2. 函数也是一个值。

在函数式编程中,函数是一等公民,也就是说,函数可以作为参数传递给其他函数,也可以作为返回值返回给其他函数。这对于学习过范函或者高等范函的人来说,应该不陌生。在

另外还有一个值的注意的是,F#不是一个纯粹的函数式编程语言,因此F#还有对象和变量,也能通过ref获得变量的地址。

let mutable y = 1.0
let z = ref y
do z.Value <- 2.0

如果是初次接触函数式编程语言,应该首先把概念局限在值的范围来解决问题,这样可以更快地理解函数式编程的思想。所以我们首先明确下面的概念:

  1. 值是不可变的;
  2. 函数也是一个值;
  3. 使用关键词let来声明值(包括函数)。

数学函数及值的类型

数学中的函数有三个核心概念:自变量的取值范围、因变量的取值范围和自变量到因变量的映射。这三个要素在F#中对应为三个概念:输入量的类型,输出量的类型,函数的值。

例如,数学函数
y = f ( x ) = x 2 y = f(x) = x ^ 2 y=f(x)=x2

直接映射到F#中,就是:

let f x = x * x

跟值的定义采用同样的语法,这样一来,函数是一个值的概念也自恰起来。因为函数是一个值,那么函数也自然有其类型。从数学函数那个看,

f : Ω → Π f : \Omega \to \Pi f:ΩΠ

对应到F#中,就是

let f  (x:int):  int = x * x
// val f : int -> int

适应值的概念,以及值的类型概念,F#被设计为静态强类型的语言,也就是在编译(从源代码到dotnet字节码或者本地机器代码)的过程中,所有类型都应该确定。为了避免像上面这个类子中一样频繁地书写类型,F#提供了类型推断的功能,也就是说,F#编译器可以根据上下文推断出类型。所以,上面的例子可以简化为:

let f x = x * x
// val f : int -> int

当发生歧义的时候,或者需要强制类型的时候,可以使用:来指定类型。

多变量函数与F#函数偏应用

在数学上,多变量函数是一个很常见的概念。例如,函数

f ( x , y ) = x 2 + y 2 f(x, y) = x ^ 2 + y ^ 2 f(x,y)=x2+y2

是一个两个变量的函数。在F#中,可以这样定义:

let f x y = x * x + y * y
// val f : int -> int -> int

从数学上来讲,这个可以定义

g ( y ) ≜ f ( x , y ) ∣ x = x ˉ = x ˉ 2 + y 2 f ( x , y ) ≜ f ( x , y ) x = x ˉ , y = y ˉ = x ˉ 2 + y ˉ 2 \begin{split} & g(y) \triangleq f(x, y)|_{x=\bar{x}} = \bar{x}^2 + y^2 \\ & f(x,y) \triangleq f(x,y)_{x=\bar{x}, y=\bar{y}} = \bar{x}^2 + \bar{y}^2 \\ \end{split} g(y)f(x,y)x=xˉ=xˉ2+y2f(x,y)f(x,y)x=xˉ,y=yˉ=xˉ2+yˉ2

对应中间的F#函数,可以定义为:

let g y = f x y
// val g : int -> int

这个函数定义可以简写为:

let g = f x
// val g : int -> int

事实上,在程序中,可以直接把(f x)当作一个函数使用,这就是F#中函数偏应用机制。

函数的组合

在数学中,函数的组合是一个很常见的概念。例如,函数

f ( x ) = x 2 f(x) = x ^ 2 f(x)=x2

g ( x ) = x + 1 g(x) = x + 1 g(x)=x+1

可以组合成

h ( x ) = f ( g ( x ) ) = ( x + 1 ) 2 h(x) = f(g(x)) = (x + 1) ^ 2 h(x)=f(g(x))=(x+1)2

在F#中,可以这样定义:

let f x = x * x
let g x = x + 1
let h x = f (g x)
// val f : int -> int
// val g : int -> int
// val h : int -> int

在F#中,函数的组合可以使用>>操作符来实现:

let h = f >> g
// val h : (int -> int)

函数的柯里化

在数学中,函数的柯里化是一个很常见的概念。例如,函数

f ( x , y ) = x 2 + y 2 f(x, y) = x ^ 2 + y ^ 2 f(x,y)=x2+y2

可以柯里化为

f ( x ) = ( y ↦ x 2 + y 2 ) f(x) = (y \mapsto x ^ 2 + y ^ 2) f(x)=(yx2+y2)

在F#中,可以这样定义:

let f x y = x * x + y * y

let f' x = (fun y -> f x y)

let f'' = (fun x -> (fun y -> f x y))

// val f : int -> int -> int
// val f' : int -> (int -> int)
// val f'' : int -> (int -> int)

函数的反柯里化

在数学中,函数的反柯里化是一个很常见的概念。例如,函数

f ( x ) = ( y ↦ x 2 + y 2 ) f(x) = (y \mapsto x ^ 2 + y ^ 2) f(x)=(yx2+y2)

可以反柯里化为

f ( x , y ) = x 2 + y 2 f(x, y) = x ^ 2 + y ^ 2 f(x,y)=x2+y2

在F#中,可以这样定义:

let f x = (fun y -> x * x + y * y)

let f' x y = (f x) y

let f'' = (fun x y -> (f x) y)

// val f : int -> (int -> int)

// val f' : int -> (int -> int)

// val f'' : int -> (int -> int)

总结

F#中的函数是一个值,也就是说,函数也是一个值。函数的类型是输入量的类型到输出量的类型的映射。函数的组合、偏应用、柯里化和反柯里化是F#中的常见操作。

这些都是函数式编程中的最基础概念,跟数学中的函数概念关系十分密切,也是函数式编程中的基础。最好是能够反复体会,不然后面看到代码时就会有各种各样的困惑。

但是只要理解值不可变的核心观念,一切都可以理解。

参考资料

  1. F# for fun and profit
  2. F# for fun and profit - Functions
posted @ 2023-07-02 21:28  大福是小强  阅读(69)  评论(0编辑  收藏  举报  来源