F#奇妙游(30):计算表达式与ADT
Computation Expression More
F#中自定义的 Computation Expression 一共有8个语法构造,其中match!
是let!
的语法糖。
在前面的一个帖子里CE初探我们已经介绍了 computation expression 中的绑定和返回,语法是let!
和return
。通过使用这两个语法,我们可以实现程序逻辑,而把程序逻辑和具体的实现分离开来。翻过去再看一遍,发现还是有点不太容易理解。于是嘛,书读百遍其义自见,我们就把这个例子再来一遍。
处理int option
运算的简单例子
在对int option
进行算术运算时,我们要对每个变量进行模式匹配,这样就会有很多的match
语句,这样的代码看起来就不太美观。这里运用 computation expression 就可以把这些match
语句隐藏起来,让代码看起来更加简洁。这样说来的目的就已经清楚了,我们需要实现一个 computation expression,这个 computation expression 能够处理int option
的运算。
还是上次的例子,不再赘述。
type Maybe() =
member this.Bind(opt, func) = opt |> Option.bind func
member this.Return v = Some v
let maybe = Maybe()
let rateStudent name =
match name with
| "isaac" -> Some 90
| "mike" -> Some 80
| _ -> None
let answer =
maybe {
let! first = rateStudent "isaac"
let! second = rateStudent "mike"
let! third = rateStudent "isaac"
let total = first + second + third
return (float total) / 3.0
}
我们再来看看这些神秘的小东西到底是什么呢?在CE初探中,我们通过对let
的语义进行分析,确定let
实际上是let pattern in expressionbody | expression
,然后才能把let!
和Maybe
的Bind
联系起来。
其实,我们可以把let!
和maybe
所构造的Computation Expression称为一种语法糖。那么从这个意义上来看,我们就能把上面代码中的计算表达式部分重构为脱糖的版本。
let answer =
maybe.Bind( //let! first = ...
rateStudent "isaac",
(fun first ->
maybe.Bind( //let! second = ...
rateStudent "mike",
(fun second ->
maybe.Bind( //let! third = ...
rateStudent "isaac",
(fun third ->
let total = first + second + third
maybe.Return ((float total) / 3.0)
)
)
)
)
)
)
从这里可以看到,let!
的展开就是let pattern = maybe.Bind(expression, func)
。如果还是看不清楚,通常都是看不清楚的,我们就可以用F#中最好用的代码分析工具:ADT!
遇事不决ADT之
如何ADT呢?前面已经强调过很多遍,所有的值都是表达式,所有的表达式都是ADT。
我们从函数的定义开始看。
type Maybe() =
member this.Bind(opt, func) = opt |> Option.bind func
member this.Return v = Some v
这里this.Bind
是什么类型呢?
如果用自己来替代F#的类型推导,过程大概就是:
this.Bind
是一个A * B -> C
的类型;- 其中
B
自己又是一个D -> F
的类型; - 根据
Option.bind
的默认类型,可以得到A
是option
,F
是option
; - 最终得到
this.Bind
是T option * (T -> T' option) -> T' option
的类型。
写得好看点,就是option<T> * (T -> option<T'>) -> option<T'>
类型。放到F# REPL中验证一下。
type Maybe =
new: unit -> Maybe
member Bind: opt: 'b option * func: ('b -> 'c option) -> 'c option
member Return: v: 'a -> 'a option
再进一步我们知道,option<T>
是一个和类型。
option<T> =
| None
| Some of T
这个类的组合数等于
1
+
C
T
1 + C_T
1+CT ,而func
参数的组合数则是
C
T
′
C
T
C_{T'} ^ {C_T}
CT′CT ,Bind
函数输入参数的组合数为
(
1
+
C
T
)
C
T
′
C
T
(1+C_T) C_{T'} ^ {C_T}
(1+CT)CT′CT ,输出参数的组合数为
C
(
T
′
)
C(T')
C(T′),整个函数的组合数为
C
T
′
(
1
+
C
T
)
C
T
′
C
T
C_{T'} ^ {(1+C_T) C_{T'} ^ {C_T}}
CT′(1+CT)CT′CT 。
btw. 我也不知道我为什么计算组合数,貌似没什么用啊。但是通过计算组合数,我们对于这个函数的理解就比较深刻了。我每次有什么函数不太理解的时候,都会用这种方式来分析一下,来来回回算几遍,就感觉理解多了,我这边建议试一试。
回到调用函数的地方
这个语法糖这么一分析就非常清楚了,我们的builder类,Maybe
其实就是为前面的语法糖提供了ADT的实现,这个ADT就定义了在计算表达式中各变量的类型和值。比如这里的三个变量first
、second
和third
,它们的类型都是int
,而不是int option
。所以这里面的let total = first + second + third
就是一个普通的算术运算,而不是int option
的运算。
但是最为神奇的是什么呢?在任何一个let!
语句中,我们都可能会得到None
,这个时候,整个计算表达式就会停止,而不会继续往下执行。这个语法在哪里实现的呢?这个语法在let!
的expression
中实现的。
member Bind: opt: 'b option * func: ('b -> 'c option) -> 'c option
中,如果opt
是None
,那么func
就不会被调用,而是直接返回None
。
那么现在有一个问题,如果那个计算表达式写成这样,会怎么样呢?
let answer =
maybe {
let! first = rateStudent "isaac"
let! second = rateStudent "mike"
let! third = rateStudent "isaac"
first + second + third
}
大家可以试一试。会报错,因为什么?因为不满足maybe.Bind
的ADT类型,maybe.Bind
的ADT类型是option<T> * (T -> option<T'>) -> option<T'>
,而这里的total
是int
类型,不是option<T'>
类型。最后那句,不用return
改成什么都报错,其实就是在计算表达式中的所有式子都脱糖后都要满足Maybe
对应函数的ADT。
组合数不同的物种,没法谈恋爱!
结论
- ADT分析是吃透F#的关键,所有的值都是表达式,所有的表达式都是ADT;
- computation expression 是一种语法糖;
- 可以回到语法糖的脱糖版本,然后用ADT分析来理解这个语法糖;
- 希望能够通过这个例子,对 computation expression 有一个更深刻的理解。