F# 函数式编程之 - 隐藏运算

“隐藏运算” 是我发明的词,它的正式名称是 “computation expressions”。

但 “computation expressions” 这个名称实在让人非常费解,也不能反映它的作用,不是一个好名称。

它的作用是在背后对两个表达式进行一些操作,让表达式们表面上看起来简单。

请看例子:

let divideBy bottom top =
    match bottom with
    | 0 -> None
    | _ -> Some <| top/bottom

let maybe = new MaybeBuilder()

let divideWorkflow init x y z =
    maybe {
        let! a = init |> divideBy x
        let! b = a |> divideBy y
        let! c = b |> divideBy z
        return c
    }

divideWorkflow 12 3 2 1  // Some(2)
divideWorkflow 12 3 0 1  // None

请看那几个 let!, 表面上是 init 除以 x, 得出结果 a 再除以 y, 以此类推。这个表面上的逻辑非常清晰,但如果不是背后隐藏了一些处理(意思是,如果用 let 而不是用 let!),这个代码是跑不通的。因为 init 除以 x 得出来的 a 不是一个数字(divideBy 的返回值是 Option, 不是 int), 因此如果让 a 直接除以 y 会产生类型不匹配的错误。

简单来说,let! 表示 “computation expressions”, 也就是意味着在背后隐藏着一些我们表面上看不见的运算。

那么,这个背后的运算具体是什么?我们可以看到,全部 let! 都在 maybe{...} 里面,而 maybe 是由 MaybeBuilder 生成的,显然,关键在于 MaybeBuilder 的定义:

type MaybeBuilder() =
    member this.Bind(x, f) =
        match x with
        | None -> None
        | Some a -> f a

    member this.Return(x) = Some x

如上所示,这个 MaybeBuilder 的定义不是系统自带的,要我们自己写。可以看到,这个 “背后的代码” 对 x:Option 进行处理,如果是 None 则直接返回 None, 并且由于这个 None 会传递给下一个 let!, 因此可以预见一旦遇到 None, 后续的 f 就一律不会被运算,最终结果返回 None。如果是 Some 则从 Some 中提出取 a:int 并执行 f(a), 因此,经过这个背后的处理,表面上的 a |> divideBy y 就不会出错了。

“computation expressions” 可以把啰嗦的辅助语句隐藏在背后,让表面上的核心语句看起来很简洁,因此很好用,是 F# 里很常用的一种技术。但由于它是 “隐式的” 而不是 “显示的”, 所以理解起来需要在脑子里多绕几个弯,本文只是简单地介召了它的主要特点,想了解得更详细请看以下资料:

  1. https://fsharpforfunandprofit.com/posts/computation-expressions-intro/
  2. https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions

更新、补充

由于标准库里也带了一些好用的函数,比如 Option.bind, 因此 MaybeBuilder 也可以改写成:

type MaybeBuilder() =
    member this.Bind(x, f) = Option.bind f x
    member this.Return(x) = Some x
posted @ 2020-12-13 22:09  cmdOptionKana  阅读(123)  评论(0)    收藏  举报