【F#入门】 第六讲 计算表达式(与异步模型简介)
学习了之前的内容后,我们能够理解在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) | 说明 |
|---|---|---|
Bind | M<'T> * ('T -> M<'U>) -> M<'U> | 在计算表达式中调用 |
BindN | (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> | 在不合并输入的情况下,在计算表达式中实现高效的 |
Delay | (unit -> M<'T>) -> Delayed<'T> | 将计算表达式包装为函数。 |
Return | 'T -> M<'T> | 在 |
ReturnFrom | M<'T> -> M<'T> | 在 |
BindReturn | (M<'T1> * ('T1 -> 'T2)) -> M<'T2> | 在计算表达式中需要高效的 |
BindNReturn | (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> | 在计算表达式中调用高效 |
MergeSources | (M<'T1> * M<'T2>) -> M<'T1 * 'T2> | 在 |
MergeSourcesN | (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> | 在计算表达式中调用 |
Run | Delayed<'T> -> M<'T> 或 | 执行计算表达式。 |
Combine | M<'T> * Delayed<'T> -> M<'T> 或 | 在计算表达式中调用排序。 |
For | seq<'T> * ('T -> M<'U>) -> M<'U> 或 | 在计算表达式中需要 |
TryFinally | Delayed<'T> * (unit -> unit) -> M<'T> | 在计算表达式中需要 |
TryWith | Delayed<'T> * (exn -> M<'T>) -> M<'T> | 在计算表达式中需要 |
Using | 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable | 调用 |
While | (unit -> bool) * Delayed<'T> -> M<'T> 或 | 在计算表达式中需要 |
Yield | 'T -> M<'T> | 在计算表达式中需要 |
YieldFrom | M<'T> -> M<'T> | 在计算表达式中需要 |
Zero | unit -> M<'T> | 在计算表达式中,调用 |
Quote | Quotations.Expr<'T> -> Quotations.Expr<'T> | 指示计算表达式作为引述传递给 |
当然,如此多的函数并不需要我们一下子掌握,大多是情况我们只需要学会使用官方提供的几种表达式,对于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: @无聊的年
更多推荐
所有评论(0)