SML学习笔记03-函数
1 函数的应用
1.1 函数的类型检查
一个 lambda 表达式 (fn (x:int) => body) : t1 -> t2
其类型是 \(t1\rightarrow t2\) 当
body : t2
且 x : t1
注意:
参数 \(x\) 的类型很重要,不能被省略
举个例子:若函数体 body
为 x + 9
,则仅当
\(x\)
的类型确定时:x : int
,函数体 body
才能具有一个合规的类型,从而整个 lambda 表达式才能有类型
int -> int
例:
- (fn (x:int) => x) : int -> int
- (fn (x:real) => x) : real -> real
如果e2 e1 : t2
仅当 e2 : t1 -> t2
且
e1 : t1
此处
e2
可看作一函数,e1
可看作函数e2
的一个参数
现已知一计算面积的函数
area
,如:area(2.1 + 1.9)
,则
area : real -> real
为何有area : real -> real
?
因为 area
是一个 lambda 表达式
fn (r:real) => pi*r*r
且 pi*r*r : real
为何 pi*r*r : real
?
因为:pi:real
且 r:real
1.2 求值规则
e2 e1的求值规则,分为以下四步:
- 将
e2
化简为一个(函数)值 - 将
e1
化简为一个值v
- 将第二步中新产生的绑定
[v/x]
添加到环境env
中 - 利用现存环境
env
中的一系列绑定去带值求解函数体body
回忆学习笔记02:
一个闭包(closure)包含了一个 lambda 表达式
(fn (x:t) => body)
以及一个环境env
而一个环境
env
由该函数在定义时其自由变量的绑定所组成
注意:
第二步会出现仅当第一步能产生一个值
第三步和第四步会出现仅当第一第二步能产生值
第四步或许会产生一个值,但也可能不会产生一个值
具体来说,例:
求 area (2.1 + 1.9)
\(\Rightarrow\)
[3.14159/pi]
(fn (r:real) => pi*r*r) (2.1 + 1.9)
\(\Rightarrow\)
[3.14159/pi]
(fn (r:real) => pi*r*r) 4.0
\(\Rightarrow\)
[3.14159/pi][4.0/r]
pi*r*r
\(\Rightarrow\)
50.26544
一个问题:
val pi : real = 3.14159
fun area (r:real) : real = pi*r*r
area (2.1 + 1.9)
\(\hookrightarrow\) 50.26544
val pi : real = 0.0
area (2.1 + 1.9)
\(\hookrightarrow\)
????????
答案仍然是
50.26544
为什么?
因为当函数 area
被定义时,pi
就已经和值
3.14159
绑定了(上述 “回忆学习笔记02”
已阐述过函数定义及其绑定间的关系),后续不会再被更改
2 递归
让我们从一个简单的阶乘函数来入手递归:
在数学中,阶乘函数定义为:
\(fact(0)=1\)
\(fact(n)=n*(fact(n-1)),\ for\ all\ n>0\)
我们也可以用 \(n!\) 这一缩写记号来代表 \(fact(n)\)
而数学中的这一阶乘函数在 SML 语言中将被写作:
1 |
|
则 fact 6
= 720
fact 0
= 1
3 模式
3.1 函数子句与模式匹配
1 |
|
上述代码中有两个函数子句:
- 第一个子句以
fun
为开头 - 第二个子句以逻辑符号 “或”
|
为开头
在这之后,每条子句都遵循此格式:fact pattern = expression
当 SML 语言对形如 fact(value)
的表达式进行求值时,SML
会尝试将 value
按顺序与每一个 pattern
进行匹配
- 如果匹配成功,只要
pattern
中包含变量,SML 就会创建一个变量绑定,并且对相应表达式进行求值
例:
- 对于
fact(0)
来说,0 会与第一个pattern
相匹配,于是 SML 求值为 1 - 对于
fact(3)
来说,3 会与第二个pattern
相匹配,并且 SML 会创建一个值绑定:[3/n]
,然后再接着去对表达式n*fact(n-1)
求值
3.2 更一般的形式
1 |
|
每一个 pj
都是一个
pattern
,且每一个
ej
都是一个表达式
注意:
如果 f : t -> t'
,则每一个 pattern
pj
都必须属于类型 t
,且每一个表达式
ej
都必须属于类型 t'
当对 f(v)
进行求值时,SML 会尝试将 v
与
p1
, p2
, ... ... , pk
进行匹配,直到遇到与 v
相匹配的 pj
,此时便停止匹配。同时对 pj
相应的表达式 ej
进行求值
如果没有 pattern
与
v
匹配,则会引发出一个
至关重要的“运行时错误”,处于这个原因,pattern
的集合 {pj}
应当覆盖所有可能的情况。
如果你给出的
pattern
集合{pj}
没有覆盖完所有的情况,SML 会给出一个 Warning: match nonexhaustive但当你给出的
pattern
集合{pj}
中出现了重复或者多余的元素,则 SML 会给出一个 Error: match redundant
3.2 什么是 pattern
?
目前(以后我们会看到更多的情况),一个 pattern
可以是以下几种情况之一:
- 一个常量:如:
3
,true
,"abc"
- 一个变量
- 子
pattern
的一个元组 - 一个通配符
_
(意为可以匹配任何东西)
patterns
必须是
线性的,这意味着任何变量在任何一个
pattern
中最多只能出现一次
4 元组
4.1
类型patterns
可以出现在声明中
例:
1 |
|
此处的 pattern
是一个元组:(k,r)
,由一对变量组成(子patterns
是两个变量)
这一行声明在环境 env
中创造了两个绑定:[2/k, 3.14/r]
例:
1 |
|
此处的 pattern
是一个常量:49
这里的声明没有包含任何一个变量。它会成功仅当 square
返回一个值 49
所以,此处的声明更多的意义是在于对函数 square
的测试
例:(元组 pattern
在函数中的应用)
1 |
|
5 case
表达式
5.1
类型pattern
出现在case
表达式中
1 |
|
case
表达式的语义样式和函数非常相像(e
可看作作函数中的参数)类型检查:
设表达式 e
类型为 t'
,则所有的
pj
必须能够与类型 t'
相匹配
所有表达式 ej
的类型必须一致,我们姑且称其类型为
t
则类型 t
就是 case
表达式整体的类型
图例:
- SML 会对
e
求值,如果e
能化简成值v
,则 SML 会将值v
与p1
,p2
, ... ... ,pk
相匹配。如果第一个匹配到的pattern
是pj
,则 SML 就对pj
后对应的表达式ej
求值
5.2
case
表达式是一种避免 if-then-else
结构的一种很好的代替方案
1 |
|
注意:
(square x, x > 0)
中的 x > 0
并不是在强调 square x
中 x
的范围。而是二元组
(square x, x > 0)
中的第二个元素(square x
为二元组中的第一个元素)
最后一行的 sqr
是一个普通的变量名,指代
case
后所跟二元组 (square x, x > 0)
的第一个元素:square x
最后两行中出现的 _
意为通配符,任何参数都能与之成功匹配
所以,我们可以得到:
1 |
|
6 函数是一类成员(值)
6.1 函数可以作为参数被使用
函数能否被当作参数使用是函数式编程语言的一个重要特征
例:
1 |
|
注意:
sqrf
的参数是一个由函数和 int
类型值组成的二元组,即 ((int -> int) * int)
那么该如何调用 sqrf
函数呢?
这就需要用到内联 lambda
表达式来构造匿名函数的方式去充当参数:
sqrf (fn (n:int) => n+2, 4);
一道例题:
1 |
|
解:
dotwice (fn (k:int) => k, 3)
= sqrf (fn(n:int) => sqrf(k->k,n), 3)
sqrf(k->k, n)
= n*n
所以:
dotwice (fn (k:int) => k, 3)
= sqrf (fn(n:int) => sqrf(k->k,n), 3)
= sqrf (fn(n:int) => n*n, 3)
= square(9)
= 81
所以:
81 = dotwice (fn (k:int) => k, 3)
7 一些补充
7.1 关于扩展等价的一些补充
- 如果 \(e_1\hookrightarrow v\) 且 \(e_2\hookrightarrow v\) ( \(v\) 是一个值),则\(e_1\cong e_2\)
- 如果 \(e_1\Rightarrow e_2\) ,则 \(e_1\cong e_2\)
- 如果 \(e_1\Rightarrow e\) 且 \(e_2\Rightarrow e\) ,则 \(e_1\cong e_2\)
- 注意:\(e_1\cong e_2\) 并不意味着 \(e_1\Rightarrow e_2\) 或者 \(e_2\Rightarrow e_1\)
例:
\(1+1+1+7\) \(vs\) \(2\times 5\)
纵使 \((1+1+1+7)\cong (2\times 5)\) 但是这并不意味着 \((1+1+1+7)\Rightarrow (2\times 5)\) 或者 \((2\times 5)\Rightarrow (1+1+1+7)\)
因为 \(\Rightarrow\) 意味着 “化简” 。而虽然上述二者都相等,但是它们却无法化简到对方的形态
[3/y, 5/z] (fn(x:int)=>x+y+z)
\(\cong\)(fn(x:int)=>x+8)
两函数虽然扩展等价,但是两函数并不相同,不能画等号
因为:在第一个函数中,\(y+z\) 的计算只有在函数被调用时才会被执行(其余的时刻 \(y+z\) 并不会被计算),所以严格来讲,\(y+z\neq 8\),意即两个函数不能严格画等号
7.2 关于完全性的一些补充
假设 f: int -> int
- 如果
f
是完全的,则f(1)+f(2)
\(\cong\)f(2)+f(1)
- 如果
f
不一定是完全的,则f(1)+f(2)
\(\ncong\)f(2)+f(1)
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!