学习了之前的内容后,我们能够理解在F#中,使用代数数据类型(抽象的数据类型)是一件非常常见的事。在函数式编程的介绍中,我们也看到了代数数据类型有多么强大的表达能力。

不过有一点我们在F#中没有谈到,尽管代数数据类型方便,但是如果有多个代数数据我们想一起组合起来运算,如果没有一个好用的工具的话,就会显得有些麻烦。

例如

let a = Some 1
let b = Some 2
let add a b = a + b

// let d = a + b // 这是无法实现的
// let d = add a b // 这是无法实现的

我们无法简单的将两个高级值进行组合计算

当然,我们可以参考c#,使用类似的方式,即lambda表达式来实现类似的功能

let ans = a |> Option.bind 
              (fun av -> (b |> Option.map ( fun bv -> av + bv ) )  )

图片

....

毫无疑问,这太麻烦了! 绝对不是我们想要的方式。也不是F#应该有的代码的样子,就算是c#当中,这个代码也不推荐使用

在这个简单的场景中,F#自带的map2还足以应付

let ans = Option.map2 add a b
let ans = Option.map2 (+) a b

map2这个函数让我们可以组合两个高级值的运算,他的内部实现是使用模式匹配,(同时也是inline,这样可以获得尽可能高的效率)

[<CompiledName("Map2")>]
    let inline map2 ([<InlineIfLambda>] mapping) option1 option2 =
        match option1, option2 with
        | Some x, Some y -> Some(mapping x y)
        | _ -> None

是的,模式匹配当然也可以实现这个功能,模式匹配是函数式编程的核心功能,其表达能力非常强。

我们也可以看到当两个值有任何一个为None时,函数则会给我们返回None。这也被称为快速失败,这便是我们期望的行为,当所有值可用的时候,我们才能得到可用的计算结果。

那么我们还有其他的方法吗?

在函数式编程的学习中,我们学习到了C#是如何处理这一个问题的

【函数式编程】(六.plus)LINQ查询表达式 如何让我们的代码变得更加优雅?-CSDN博客

C#通过Linq查询表达式这一语法,轻松的解构了各种各样的高级值,让其在最后部分中轻松的组合所有结果,那么F#有类似的方式吗?我们能做的更好吗?

当然是有的,不过F#默认并没有对Option实现该功能,不过我们依然能自己实现它。

计算表达式

其实在F#中,有默认实现的版本,例如异步表达式,任务表达式等。不过并没有一些函数式编程常用类型的实现版本。官方似乎是鼓励这些由第三方库实现,而F#本身核心库仅保留最小的功能选择。

(例如 FsToolkit.ErrorHandling)

其实这与linq查询表达式一样,是有一些鸭子类型的感觉,或是说是一些魔法操作

当某个类型实现了某些签名的函数之后,编译器就会支持这种语法使用。

回想起在函数式编程中介绍的,单子所定义的几个函数。

Bind 与Return

那么,现在让我们定义 OptionBuilder

type OptionBuilder() =  
    member _.Bind(m, f) = Option.bind f m  
    member _.Return(x)  = Some x  

由于自带的Option类型拥有bind函数,我们只需简单封装即可

现在我们完成了定义,具体有什么作用呢?我们继续往下走。

当我们定义了OptionBuilder之后,我们就可以进行一种特殊的操作

let option = OptionBuilder()
let result =  
    option {  
        let! x = a  
        let! y = b  
        return x + y
    } 

在option的计算表达式中,我们可以用let!来直接获取a,b中的值,最后使用return返回x+y的值。

如果你问这时候,a或者b没有值怎么办?那当然就是不办了,与我们之前预期的行为一样,直接返回None,这便是快速返回,当所有值有效时才可以得出有效结果。

在这个计算表达式中,我们就可以自由的编写自己想要的处理方式,与正常的处理方法几乎无异,没有任何高级值之间计算会遇到的烦恼,

那么我们为什么要定义OptionBuilder呢,其中let!的能力又是哪里来的?

正如C#中from语句需要SelectMany的函数实现,这里的let! 扮演的就是from语句的角色,而先前定义的Bind函数,就是我们使用let!时,内部所需要的函数。事实上,使用return 也需要我们Return函数的实现。这样我们就实现了一个单子所需要的函数签名,是不是与我们先前学习理论完全一致呢?

有了计算表达式之后,我们就能更好的发挥函数式编程的强大能力

不过,计算表达式的能力事实上还能更上一层楼,我们只是使用了其中两个小函数。

这也是为什么这里的return不能像其他代码中一样省略的原因,在计算表达式中,我们还有 return! yield等返回方式,我们需要明确的指出是如何返回一个值的方法

见下表(官方文档),这些都是计算表达式中支持的函数功能

方法典型签名(s)说明
BindM<'T> * ('T -> M<'U>) -> M<'U>

在计算表达式中调用 let! 和 do!

BindN(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U>

在不合并输入的情况下,在计算表达式中实现高效的 let! 和 and!

例如,. Bind3Bind4

Delay(unit -> M<'T>) -> Delayed<'T>

将计算表达式包装为函数。 Delayed<'T> 可以是任何类型,通常 M<'T> 或 unit -> M<'T> 被使用。 默认实现返回一个 M<'T>

Return'T -> M<'T>

在 return 计算表达式中调用。

ReturnFromM<'T> -> M<'T>

在 return! 计算表达式中调用。

BindReturn(M<'T1> * ('T1 -> 'T2)) -> M<'T2>

在计算表达式中需要高效的let! ... return

BindNReturn(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U>

在计算表达式中调用高效 let! ... and! ... return ,无需合并输入。

例如,. Bind3ReturnBind4Return

MergeSources(M<'T1> * M<'T2>) -> M<'T1 * 'T2>

在 and! 计算表达式中调用。

MergeSourcesN(M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN>

在计算表达式中调用and!,但通过减少元组节点的数量来提高效率。

例如,. MergeSources3MergeSources4

RunDelayed<'T> -> M<'T>

 或

M<'T> -> 'T

执行计算表达式。

CombineM<'T> * Delayed<'T> -> M<'T>

 或

M<unit> * M<'T> -> M<'T>

在计算表达式中调用排序。

Forseq<'T> * ('T -> M<'U>) -> M<'U>

 或

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>

在计算表达式中需要 for...do 表达式。

TryFinallyDelayed<'T> * (unit -> unit) -> M<'T>

在计算表达式中需要 try...finally 表达式。

TryWithDelayed<'T> * (exn -> M<'T>) -> M<'T>

在计算表达式中需要 try...with 表达式。

Using'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable

调用 use 计算表达式中的绑定。

While(unit -> bool) * Delayed<'T> -> M<'T>



(unit -> bool) * Delayed<unit> -> M<unit>

在计算表达式中需要 while...do 表达式。

Yield'T -> M<'T>

在计算表达式中需要 yield 表达式。

YieldFromM<'T> -> M<'T>

在计算表达式中需要 yield! 表达式。

Zerounit -> M<'T>

在计算表达式中,调用else表达式中if...then空的分支。

QuoteQuotations.Expr<'T> -> Quotations.Expr<'T>

指示计算表达式作为引述传递给 Run 成员。 它将计算的所有实例转换为引用。

当然,如此多的函数并不需要我们一下子掌握,大多是情况我们只需要学会使用官方提供的几种表达式,对于option等类型,也有第三方库提供我们相应的实现。

异步表达式

说了这么多函数式编程的option, 有可能你依然云里雾里,那我们来看看C#程序员会更熟悉的async, task

Async<T> 不是一个"已经包含未来T的值"的容器,而是一个计算描述,当被启动时,它会在未来某个时间点产生一个 T 类型的值。

这当然也是一个高级值,是一个类型T的抽象!如果我们有多个异步的值要使用,是不是也会觉得困难呢?这其实与我们option所遇到的困境是一样的!在C#中,await语法一定程度上避免了这个困境,但是当然也带来了一定的问题(例如async标记的病毒式传播)

但如果我们只是把他看成一个高级值,是不是就没有这个问题了呢?

我完全无需关心函数是不是异步的,Async<T>作为返回值的函数与其他函数一样,也只是返回一个值罢了,只不过我们返回的是一个计算步骤,本身函数并不是什么特殊的函数

你这也不行啊?我C#还能通过await等方式最后组合我们的计算结果,你这该咋办

聪明的同学肯定发现了,我们索要做的不就是将,Async<T>的高级值和作为T的普通值或是同为Async<T>进行运算吗?

那我们的计算表达式,便可以完美的达成这个需求(当前其他类似的函数式操作都可以做到)

let aa = async {
    return 1
}
let bb = async {
    let! av = aa
    let! av1 = aa
    return av1 + av
}

非常轻而易举!这种特殊的表达式也是我们F#中的异步表达式

学会了吗?学会了!

不过F#为了照顾C#的task类型,还有任务表达式

let aaT = task {
    return 1
}
let bbT = task {
    let! av = aaT
    and! av1 = aaT
    return av1 + av
}

用法几乎如出一辙,不过这里有一个特殊的and, 涉及多个高级值的绑定时,如果能使用and!进行连续绑定, 其会有更高的执行效率。

这便是我们的计算表达式

微信公众号: @scixing的炼丹房

Bilibili: @无聊的年

【F#入门】(六)计算表达式(异步表达式)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1PcHLzsEg5/?spm_id_from=333.1387.homepage.video_card.click

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐