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,非常神奇。

优点

上面这种实现的优点有哪些?

  1. 在maybe中,let!绑定变量的类型是int而不是int option;
  2. 后续的计算,完全不用处理option的问题,就当做是没有option(龟毛老师)这回事;
  3. 返回的类型是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函数的表达式为什么是那么样子,而上面的计算表达式中,firstsecondthirdtotal都是int,而不是int option了。

结论

  1. 处理int option的Builder,把int option的计算表达式,转换成了int的计算表达式;
  2. 这就是计算表达式的作用,让我们在上下文中编写表达式,表达我们所想要关注的计算,把option的处理逻辑放在上下文中隐含处理;
  3. 这里只涉及到两个表达式let!return,更多的构造,接下来再说。
posted @ 2023-09-04 13:29  大福是小强  阅读(27)  评论(0编辑  收藏  举报  来源