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!MaybeBind联系起来。

其实,我们可以把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#的类型推导,过程大概就是:

  1. this.Bind是一个A * B -> C的类型;
  2. 其中B自己又是一个D -> F的类型;
  3. 根据Option.bind的默认类型,可以得到AoptionFoption
  4. 最终得到this.BindT 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} CTCTBind函数输入参数的组合数为 ( 1 + C T ) C T ′ C T (1+C_T) C_{T'} ^ {C_T} (1+CT)CTCT ,输出参数的组合数为 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)CTCT

btw. 我也不知道我为什么计算组合数,貌似没什么用啊。但是通过计算组合数,我们对于这个函数的理解就比较深刻了。我每次有什么函数不太理解的时候,都会用这种方式来分析一下,来来回回算几遍,就感觉理解多了,我这边建议试一试。

回到调用函数的地方

这个语法糖这么一分析就非常清楚了,我们的builder类,Maybe其实就是为前面的语法糖提供了ADT的实现,这个ADT就定义了在计算表达式中各变量的类型和值。比如这里的三个变量firstsecondthird,它们的类型都是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中,如果optNone,那么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'>,而这里的totalint类型,不是option<T'>类型。最后那句,不用return改成什么都报错,其实就是在计算表达式中的所有式子都脱糖后都要满足Maybe对应函数的ADT。

组合数不同的物种,没法谈恋爱!

在这里插入图片描述

结论

  1. ADT分析是吃透F#的关键,所有的值都是表达式,所有的表达式都是ADT;
  2. computation expression 是一种语法糖;
  3. 可以回到语法糖的脱糖版本,然后用ADT分析来理解这个语法糖;
  4. 希望能够通过这个例子,对 computation expression 有一个更深刻的理解。
posted @ 2023-09-08 16:04  大福是小强  阅读(23)  评论(0编辑  收藏  举报  来源