|
|
本帖最后由 dasasdhba 于 2026-4-18 17:27 编辑
3.5 联合类型与 Railway
对于熟悉枚举类型的读者,可以简单将联合类型理解为增强的枚举类型:你可以给每一个枚举类附带一个数据,例如:
enum Result:
Ok A
Error B
是封装了 Ok 和 Error 两个枚举值的联合类型,其中 Ok 可以带有数据类型 A,Error 可以带有数据类型 B。这样,给你一个 Result 类型,你总是要通过 if 或者 switch,或者所谓的模式匹配,去判断它首先到底是 Ok 还是 Error,然后再拿到具体的值。设计这样的类型有什么好处呢?试想,程序运行的过程中经常会产生一些错误 Error,比如出现了 1 / 0 等等;可能发生的错误是需要尽可能处理的,不然程序就闪退了。而一般的,发生错误之后,你通常需要中断一系列的逻辑。比如说, MarioWalk 执行的时候卡墙了,你通常会期望后面的 MarioJump 等一系列的逻辑暂时中断,等待卡墙逻辑执行完成。如何实现中断呢?一个最简单的方式就是改写函数签名,使得函数在正常执行之后返回 A,否则返回 Error:
MarioWalk : A -> Result<A,Error>
MarioJump : A -> Result<A,Error>
MarioHurt : A -> Result<A,Error>
然后,编写一个辅助函数 bind: (A -> Result<A,Error>) -> Result<A,Error> -> Result<A,Error>,看上去很晕?其实就是把一个 f: A -> Result<A,Error> 变成一个 f': Result<A,Error> -> Result<A,Error>,如何实现呢?只需要讨论参数 a: Result<A,Error> 到底是 Ok 还是 Error,如果是 Ok,那么直接将 a.Ok.Value 喂给 f;若不然,直接返回 a(即直接返回 Error)。这样一来,我们的函数链就可以改写为:
Ok mario
|> bind MarioWalk
|> bind MarioJump
|> bind MarioHurt
注:
1. 读者可自行验证,bind MarioWalk 的签名是 Result<A,Error> -> Result<A,Error>
2. 这里的 bind 函数,通常能在函数式编程语言的标准库里面找到,所以也不用自己写
这样,只要中间哪一步返回的 Result 类型是 Error,那么整个链条所做的事情就是一直将这个 Error 传递下去,这就实现了逻辑的中断,且同时保留了错误信息。这样的编程风格就是所谓的 Railway:为了在发生错误时中断逻辑,我们并没有引入任何的状态量,而是将整个链条分为 Ok 和 Error 两个 Railway,如果是 Ok,则正常运行;如果是 Error,这里我们的选择就是直接返回 Error。这种编程风格当然还有更多更广泛的应用,这里只是举个例子;一般地,你可以根据自己的需要,设计多个 Railway,然后通过类似的手法将它们链接起来,这就在既保持结构清晰的同时,又避免了过多的状态量引入。 |
|