F#奇妙游(26):计算表达式浅尝
computation expression之一问三不知
计算表达式是一个有点难理解的东西。我把帮助全部看了一遍,记住了个上下文敏感的计算(contex-sensitive computation)。但是让我讲计算表达式是什么?为什么?怎么做?我是满头雾水。我大概知道是什么,就是一个语法特征,在一个表达式上下文中,使用诸如let!
,return
这些语法,实现与上下文紧密相关的特殊计算。我甚至还能侃侃而谈上下文相关、如何构造某个特殊表达式,match!
是let!
的语法糖之类的。
到底为什么?我不能回答。
奇妙的option计算
还是让我们从一个实际的问题出发。我教了一个班,只有两个学生我有点点满意,一个叫isaac
,一个叫mike
,其他人我根本不想理会。要实现一个全班评价的函数。
let rateStudent name =
match name with
| "isaac" -> Some 90
| "mike" -> Some 80
| _ -> None
这个函数的ADT就是:val rateCustomer: name: string -> int option
。
下面的问题是:有两个学生,问我给他们评价的总分是多少?我这个人比较龟毛,只有有一个学生是我不想理会的,那么我就不置可否:None
。
实现起来看起来也挺简单:
let commentTwo name1 name2 =
let result1 = rateStudent name1
let result2 = rateStudent name2
match result1, result2 with
| Some r1, Some r2 -> Some(r1 + r2)
| _ -> None
或者,我们在学会一个Option.bind
之后,还能写成:
let commentTwoAlt name1 name2 =
let result1 = rateStudent name1
let result2 = rateStudent name2
result1 |> Option.bind (fun x ->
result2 |> Option.bind (fun y ->
Some (x + Y)))
这里面,我们的Option.bind
的原型就是在MSDN中给出为bind f inp = match inp with None -> None | Some x -> f x
。
这两种方式都行,如果两个学生都是我认可的,那么给出答案Some 170
,其他任何情况,我都是不予理会:None
。
接下来事情可能变得有趣起来。假设我有100个学生,其中50个我觉得差强人意,评了个分数,其他的50个继续是不予理会。
怎么办?如果我不是求两个学生的和,而是要进行其他的计算,怎么办?
Computation Expression版龟毛老师
这里的问题挺简单,就是要处理int option
和一个返回int option
的函数rateStudent
。
首先,我们定义一个Builder。
type Maybe() =
member this.Bind(opt, func) = opt |> Option.bind func
member this.Return v = Some v
这个Builder定义了两个操作,一个是绑定,一个是返回。然后实例化一个builder。
let maybe = Maybe()
接下来就能开心的用计算表达式来处理任意复杂的计算:
let answer =
maybe {
// binding int option to int
let! first = rateStudent "isaac"
let! second = rateStudent "mike"
let! third = rateStudent "isaac"
// calculation using int
let total = first + second + third
// return float option from float
return (float total) / 3.0
}
上面这个值,是一个float option
,当我们计算中任何一个rateStudent
返回None
,计算表达式就会马上输出None。
我们在上面插入一些printfn
,更改rateStudent
的参数,就会发现,只要任何一个地方出现None
的绑定,马上就返回None
。
let answer =
maybe {
printfn "0"
let! first = rateStudent "isaac"
printfn "1"
let! second = rateStudent "mike"
printfn "2"
let! third = rateStudent "isaac"
printfn "3"
let total = first + second + third
printfn "return"
return (float total) / 3.0
}
如果第一个名字isaac
打错的话,就只会打印0
,把None
绑定到answer
,非常神奇。
优点
上面这种实现的优点有哪些?
- 在maybe中,let!绑定变量的类型是int而不是int option;
- 后续的计算,完全不用处理option的问题,就当做是没有option(龟毛老师)这回事;
- 返回的类型是float,而不需要写作Some float。
疑点
这里的Return
比较容易理解,就是把一个值包装成Some
。但是Bind
是怎么工作的呢?我们看看Maybe
的定义:
type Maybe() =
member this.Bind(opt, func) = opt |> Option.bind func
这里我们调用的时候let! first = rateStudent "isaac"
,第一个值好理解,是一个int option
,刚好符合Option.bind
的第一个参数。但是第二个参数func
是什么呢?
这里的疑问其实是let
在F#中特殊用途。按照函数式编程的概念,并没有什么全局变量的概念。而let
看起来是定义了一个全局变量。
let x = 10
printfn "%A" x
定义变量,访问变量,多么熟悉啊。但是这里的let
,其实是一个语法糖,它的本质是一个函数调用。
let x = 10 in
printfn "%A" x
或者
let x = 10 in printfn "%A" x
本质上,是:
10 |> (fun x -> printfn "%A" x)
这样写就清楚多了。
那么我们这下就能够明白,为什么定义绑定的时候,第二个参数是一个函数了。因为let!
的本质是一个函数调用,而Option.bind
的第二个参数就是一个函数。实际上,这里的函数实际上就是后续的所有表达式组成的一个函数。
let answer =
maybe {
let! first = rateStudent "isaac" in
let! second = rateStudent "mike" in
let! third = rateStudent "isaac" in
let total = first + second + third in
return (float total) / 3.0
}
或者
maybe.Bind (rateStudent "isaac") (fun first ->
maybe.Bind (rateStudent "mike") (fun second ->
maybe.Bind (rateStudent "isaac") (fun third ->
let total = first + second + third
return (float total) / 3.0)))
这样的话,我们就更清楚Bind
函数的表达式为什么是那么样子,而上面的计算表达式中,first
,second
,third
,total
都是int
,而不是int option
了。
结论
- 处理
int option
的Builder,把int option
的计算表达式,转换成了int
的计算表达式; - 这就是计算表达式的作用,让我们在上下文中编写表达式,表达我们所想要关注的计算,把option的处理逻辑放在上下文中隐含处理;
- 这里只涉及到两个表达式
let!
,return
,更多的构造,接下来再说。