Swift中的面向铁路的编程

在本文中,我们将讨论处理函数执行的两个轨道:快乐路径和错误路径。

几乎每个编程任务都由两个用户的轨迹或路径组成。 当一切顺利时,我们称其为“幸福之路”。 但是每次发生某些事情时,尤其是在移动应用程序中。 验证错误,互联网消失,系统杀死我们的应用程序,硬件报告错误,服务器不可用等。 通常,错误路径需要付出更大的努力。 我们应该以某种方式处理所有预期的和意外的错误。 铁路定向编程(ROP)可以帮助我们完成此例程。

该术语的最初发明者是Scott Wlaschin。 他是https://fsharpforfunandprofit.com/的创建者,并且是《 Domain Modeling Made Functional》一书的作者(强烈推荐给所有人)。 在他很少使用F#语言讨论ROP之后,许多社区开始对所有其他语言(而不仅仅是功能语言)采用这种方法。 现在,我想展示一下我们如何在Swift中做到这一点。


让我们从问题开始。 假设我们有一项添加基本用户注册功能的任务。 用户必须提供电子邮件和密码,它们必须有效,并且系统中不应有任何重复的电子邮件。

好的,让我们为注册功能建模。

我们为输入数据和用户创建简单的结构。 CustomDebugStringConvertible协议将有助于打印输出,而UserError枚举描述了注册过程中所有预期的和意外的错误。 这非常简单明了,让我们继续。

验证之后,我们希望将用户保存到数据库或将其发布到Web服务。 再次,为简单起见,我决定使用一个模拟保存到数据库的函数。

saveToDb(_ 🙂函数中,我们尝试创建新用户并将其放入商店,并在发现重复的情况下引发错误。

好,准备完毕。 现在让我们做第一个尝试编写寄存器功能的尝试。

首先,让我们看一下签名(UserInput)引发-> User。 我们希望接收输入并返回一个新用户。 但是我们也想以某种方式返回所有可能的错误。 一种方法是抛出错误 。 但是通常抛出错误意味着意想不到的事情刚刚发生。 我们期望电子邮件可能无效吗? 是。 我们期望密码可能无效吗? 是。 我们是否期望可能已经存在具有相同电子邮件的用户。 是。 那么,为什么不只将用户和预期错误之一一起返回呢? 人们称这种类型为元组。 Swift可能支持元组。

新的签名是(UserInput)->(User ?, UserError?) 更好,但是现在我们必须对UserUserError使用Optionals 。 这是因为我们可以返回用户错误,但不能同时返回两者。 熟悉函数式编程的人可以说:“我们在这里需要求和类型!”。 他们将是正确的。 而且我们不必发明新的东西,为此已经存在良好的抽象。 这就是结果。

简单但功能强大的Result类型。 它只有两种可能的状态:对象类型为T的 .ok (成功路径),对象类型为E的 .error (错误路径)。 很酷,现在重构我们的功能。

哦,好多了。 我们将完全返回所需的内容,而不会引发任何错误。 我们可以像这样使用此功能。

在这里,我们看到了Result用法的良好副作用。 它强制在呼叫方处理两种情况。 这意味着必须以某种方式处理所有错误。


我们的寄存器(输入:)看起来不错,但并不完美。 有什么问题? 它做很多事情。 它验证电子邮件,验证密码并将用户保存到数据库。 让我们轻松提取所有不同的逻辑部分以分离功能。

哦,看起来很丑,对吗? 有什么问题? 有人称其为if-else-nesting-hell。 那是因为我们用switch替换或保护了语句。 是的,这就是类型安全错误处理的代价。 好吧,我们实际上可以将验证函数的签名更改为(String)-> Bool并返回保护语句。 使用这种方法,我们将失去一致性。 现在让我们谈谈如何摆脱这种丑陋的嵌套。

首先,我们需要考虑到底要实现什么。 我们有顺序函数调用。 每次调用都会返回两轨结果,我们只想通过“快乐之路”走得更远,如果发生错误,则停止执行并返回错误。 理想情况下,我们想写这样的东西。

它无法编译,但我们需要功能强大且美观的东西。 这种功能组合称为“铁路定向编程”。

上面的图片说明了所需的行为。 绿色轨道-幸福的道路,红色-错误路径。 但是我们究竟如何实现这种功能组合呢?

链函数组合(或链计算)与Monad耦合。 对,现在我们正在向Monads世界迈进。 我们可以将Monad看作是某种类型的特殊容器,它可以创建链式计算。 而这种能力是通过所谓的绑定功能来完成的。 如果我们仅将该函数添加到Result类型,我们将获得Monad。

它到底是做什么的? 如果当前Resul的大小写为.ok,则调用函数,该函数将类型T的未包装值映射到新的Result。 如果当前Result的情况是.error,我们将其通过。 这正是我们所需要的。 让我们使用单子组合来重写函数。

更好,但是在最后一个绑定闭包中我们仍然有一些难看的代码。 为了使其更具可读性,我们可以将其移至单独的功能。

完善! 现在,我们可以清楚地知道该功能的作用:首先验证电子邮件,然后验证密码,最后将用户保存到数据库。 并且,如果其中任何一个函数返回错误,则执行将停止,并返回带有错误的结果。


即使最新的重构功能已经足够好了,我还是想提出一些其他的技术。

在功能世界中,使用了许多奇怪的运算符。 但是其中一些可能非常方便。 例如,我们可以使用特殊运算符来构成单子函数。 它称为Kleisli合成物或Kleisli箭头。 让我们定义自定义运算符,然后看看它的外观。

现在,我们更加独特地阅读了我们的函数组成(如果人们知道“ fish”运算符的含义是)。 这等效于带有绑定的先前版本,但没有括号。

同样在F#和许多其他功能语言中,我们还有简洁的管道前导运算符|> 。 它帮助我们编写从左到右可以像书一样阅读的代码。 让我们定义该运算符并重构我们的函数。

首先,让我们看一下运算符的定义。 管道前移运算符就像使用参数调用函数一样简单,但是我们可以用x |> f代替f(x) ,它是相同的,但是从左到右读取。 另外,我们需要为自定义运算符定义正确的优先级。 通过管道前向运算符传递参数应该在Kleisli组成之后进行,这就是为什么我们将Kleisli运算符的优先级定义为比thanPipeOperatorPrecedence高 。 现在, register(input 🙂函数是参数和三个不同函数的简单易读的组成。 甜!


这种方法对于领域建模将非常有帮助。 它可以帮助我们严格定义函数调用的所有可能结果(包括错误)。 精巧的编写有助于我们以更少的样板编写易于阅读的代码。 但是要提防自定义运算符! 它们像危险一样有用! 每个定制操作员都应与所有团队成员进行讨论。

完整的要点可以在这里找到。

编码愉快!