函数式编程:使用 F# 计算表达式编写代码
F# 语言我最喜欢的特性之一是计算表达式。
微软将其解释为“一种方便的语法,用于编写可以使用控制流构造和绑定进行排序和组合的计算。...与其他语言(例如 Haskell 中的 do-notation)不同,它们不依赖于单一的抽象,也不依赖于宏或其他形式的元编程来实现方便且上下文相关的语法。”
通俗地说,它们是一种非常酷的抽象,允许在代码流中进行许多超强大的转换和操作。在这篇文章中,我探讨了 F# 中的函数式编程,并分享了可用于简化代码的有用计算表达式。
目录
F# 中的计算表达式是什么?
从本质上讲,计算表达式是一种具有成员的类型,它接受某种类型的值并以某种方式转换这些值。有各种各样奇特的函数式编程术语来描述正在发生的事情(你会看到术语 monad 被大量使用),但你实际上并不需要了解所有这些来使用和获取计算表达式的值。
为什么使用 F# 计算表达式?
使用计算表达式的原因有很多,但主要好处之一是可读性。在函数式编程中,使用可区分联合和记录类型非常常见。对于这些类型,您通常会执行映射操作以将值从一种类型转换为另一种类型(例如SomeType<T> -> SomeType<U>)。计算表达式允许我们以一种可以真正提高这些操作可读性的方式执行此类常见操作。
从本质上讲,计算表达式所做的就是使用特殊语法调用函数。事实上,大多数计算表达式都会有一个模块,用于公开实现函数,当这样做更有意义时,可以直接调用这些函数。
但是计算表达式语法允许我们以更具声明性的方式编写一些代码,有时这非常有益。例如,使用FsToolkit.ErrorHandling 库的以下函数完全等效:
// Without computation expression
let addResult: Result<int, string> =
tryParseInt "35"
|> Result.bind(fun x ->
tryParseInt “5”
|> Result.bind(fun y ->
tryParseInt "2"
|> Result.bind(fun z ->
add x y z
)
)
)
// With computation expression
let addResult: Result<int, string> = result {
let! x = tryParseInt "35"
let! y = tryParseInt "5"
let! z = tryParseInt "2"
return add x y z
}
内置 F# 计算表达式
首先,让我们探索一些您可能已经在使用但并不知情的内置 F# 计算表达式。
seq 计算表达式
F# 中最简单的例子可能就是内置的seq计算表达式,它与C# 中的 IEnumerable<T>接口完全等同。
seq实际上不是 F# 中的关键字。它只是一个内置计算表达式,定义创建值序列所需的成员。它允许您编写如下代码:
// yield statements are technically unnecessary here, but I'm including them for clarity in the comparison
let mySequence(): int seq =
seq {
yield 1
yield 2
yield 3
}
等效的 C# 代码为:
IEnumerable<int> MySequence()
{
yield return 1;
yield return 2;
yield return 3;
}
异步和任务
其他有用的内置计算表达式包括async和task。没错,与 C# 不同,在 C# 中,你依靠编译器魔法将async转换为状态机,而在 F# 中,它只是计算表达式。(好吧, task的实现确实依赖于一些编译器魔法,但这只是一种优化。)
编写自定义计算表达式:可选
一个非常简单的自定义计算表达式的绝佳示例是可选的。我们想要的是一种更简洁的方式来处理 ' t 选项 值。如果我们有几个可能存在也可能不存在的东西,并且只有当它们都存在时我们才能进行完整的操作,我们可以使用可选的来使代码更简洁。
开始之前:理解 Option<'T> 类型
简单介绍一下背景知识,F# 有一个内置类型,名为Option<'T>。类型定义如下:
type Option<'T> =
| Some of 'T
| None
这通常用于模拟一个值可能存在或不存在的情况,类似于在许多其他语言中使用null 的方式 (但 F# 试图避免十亿美元的错误)。
由于 F# 语言的工作方式,我们可以对可选值使用模式匹配,无论值是否存在,都可以轻松、干净地以不同的方式处理事情:
let doSomething (maybeValue: string option) =
match maybeValue with
| Some value -> printfn $"Value was {value}"
| None -> printfn "No value provided"
这真是太棒了!我们保证值存在,并在同一操作中检索该值。如果值不存在,我们会有一个单独的分支来处理这种情况。
但是,当存在多个可选值时,事情可能会变得有点麻烦,我们需要在继续之前检查所有可选值是否存在。例如,假设我们有一段这样的代码:
let doSomething (a: string option) (b: int option) (c: float option) =
match a with
| Some a' ->
match b with
| Some b' ->
match c with
| Some c' ->
doSomethingWithAll a' b' c' |> Some
| None -> None
| None -> None
| None -> None
这里有很多解包操作。这里发生的事情非常明确,但读起来也相当笨拙。在这个例子中,我们实际上只有在所有可选值都是Some(x)时才能继续,否则我们只想返回None。
如果我们能够针对“快乐路径”优化代码,并在最后处理None情况,那就太好了。这就是可选 计算表达式可以派上用场的地方。
步骤 1:创建计算表达式生成器
首先我们需要定义一个构建器类:
type OptionalBuilder() =
member _.Bind(x : 'a option, f: 'a -> 'b option) = Option.bind f x
member _.Return(x: 'a) = Some x
这个类只有两个成员:Bind和Return。Bind 函数如果选项是Some,它将解开其内部值,如果选项是None,它将返回None。Return本质上只是将值包装在Some中。
用于计算表达式的成员名称具有明确定义的模式,但由于高级类型的性质,以及任何计算表达式可能不需要所有可用功能的事实,它们目前无法通过简单的接口进行建模。
因此,构建器类不需要实现接口,只需定义相关的成员方法即可。您可以看到哪些方法可用于此目的,并具有一流的支持。
步骤 2:创建实例
一旦我们有了构建器类,我们就会创建它的一个实例,并且该实例将成为我们在计算表达式中使用的“关键字”:
let optional = OptionalBuilder()
步骤 3:使用可选计算表达式重写
现在我们可以使用可选的计算表达式来重写我们的代码:
let doSomething (a: string option) (b: int option) (c: float option) =
optional {
let! a' = a
let! b' = b
let! c' = c
return doSomethingWithAll a' b' c'
}
更简洁了!现在我们可以专注于快乐之路,如果任何选项为None,我们将从 整个函数中返回None 。
总结:使用 F# 进行函数式编程
开箱即用,F# 计算表达式可用于支持您期望从 monad 类型获得的标准函数式编程操作。使用内置计算表达式函数可以做更多的事情,您还可以利用自定义操作来创建自己的领域特定语言 (DSL)!
计算表达式非常强大,我在创建简单的 SQL 查询生成器、http 请求生成器和依赖项注入容器注册时都用到了它们。试试看你能用它们构建什么!
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~