【问题标题】:When is it right time to throw an exception in functional programming在函数式编程中什么时候抛出异常合适
【发布时间】:2022-11-19 00:29:06
【问题描述】:

假设我有一个 UserController 的 Web 应用程序。客户端发送即将由控制器处理的 HTTP POST 请求。然而,首先必须将提供的 json 解析为 UserDTO。出于这个原因,存在一个UserDTOConverter 和一个方法toDTO(json): User。

鉴于我重视函数式编程实践的引用透明性和纯函数的好处,问题是。处理可能无法解析的 json 的最佳方法是什么?第一个选项是抛出异常并在全局错误处理程序中处理它。无效的 json 意味着出现了严重的错误(例如黑客)并且这个错误是不可恢复的,因此异常是正确的(即使假设 FP)。第二种选择是返回 Maybe<User> 而不是 User。然后在控制器中我们可以根据返回类型返回 HTTP 成功响应或失败响应。最终,这两种方法都会导致相同的失败/成功响应,但哪一种更可取呢?

另一个例子。假设我有一个 Web 应用程序需要从远程存储库 UserRepository 检索一些数据。来自 UserController 的存储库称为 getUser(userId): User。同样,在提供的 id 下处理可能不存在的用户错误的最佳方法是什么?我可以再次返回Maybe<User>,而不是返回User。然后在控制器中可以通过例如返回“204 No Content”来处理此结果。或者我可以抛出异常。代码保持引用透明,因为我再次让异常一直冒泡到全局错误处理程序(没有 try catch 块)。

而在第一个示例中,我更倾向于抛出异常,而在后一个示例中,我更愿意返回 Maybe。异常会导致更清晰的代码,因为代码库不会被无处不在的Eithers、Maybes、空集合等弄得乱七八糟。但是,返回这些类型的数据结构可确保调用的明确性,并且 imo 可以更好地发现错误。

函数式编程中有异常的地方吗?使用异常而不是返回Maybes 或Eithers 的最大陷阱是什么?在基于 FP 的应用程序中抛出异常有意义吗?如果是这样,是否有经验法则?

【问题讨论】:

  • Maybe/Either 是编码短路概念的两种类型。根据用法,这也可能意味着总是在您的程序中捕获异常。不同之处在于,命令式异常是一种独特的语言构造,专门设计用于对预期异常进行编码,而 Maybe/Either 是一流值的可区分联合类型。前者是引用不透明的,后者是透明的,后者更普遍 bc 短路并不一定意味着异常,但也意味着非确定性或结果缺失。

标签: exception error-handling functional-programming


【解决方案1】:

你问的是几种不同的情况,我会尝试解决每一种情况。

输入

第一个问题涉及将 UserDTO(或一般来说,任何输入)转换为更强的表示形式(User)。这样的转换通常是自包含的(没有外部依赖项),因此可以实现为 pure function。最好的方式view such a function is as a parser。

通常,解析器将返回 Either 值(也称为 Result),例如 Either<Error, User>。然而,Either monad 是短路的,这意味着如果输入有多个问题,则只有第一个问题会被报告为错误。

验证输入时,您通常希望收集并返回所有问题的列表,以便客户端可以解决所有问题并重试。 monad 不能这样做,但是 applicative functor 可以。总的来说,我相信validation is a solved problem。

因此,您需要将验证建模为与 Either 同构的类型,但具有不同的应用函子行为,并且没有 monad 接口。上面的链接已经显示了一些示例,但这里有一个真实的 C# 示例:An applicative reservation validation example in C#。

资料存取

数据访问是不同的,因为您希望数据已经有效。但是,由于两个不同的原因,从数据存储中读取数据可能会“出错”:

  • 数据不存在
  • 无法访问数据存储

第一个问题(查询丢失的数据)可能由于各种原因而发生,通常应该为此做好计划。因此,对用户的数据库查询应该返回Maybe<User>,向客户端表明它应该准备好处理两种情况:用户在,或者用户不在。

另一个问题是数据存储有时可能无法访问。这可能是由网络分区或数据库服务器遇到问题引起的。在这种情况下,通常客户端代码对此无能为力,因此我通常不会费心显式地对这些场景进行建模。换句话说,我会让实现抛出一个异常,而客户端代码通常不会捕获它(除了记录它)。

简而言之,只抛出不太可能被处理的异常。使用 sum types 表示预期的错误。

【讨论】:

    【解决方案2】:

    长话短说

    如果代码库中到处都是 Maybes/Eithers,那么您通常会遇到 I/O 与业务逻辑混杂在一起的问题。如果您用异常替换它们(反之亦然),情况也不会好转。


    Mark Seemann 已经给出了一个很好的答案,但我想解决一个具体问题:

    异常会导致更清晰的代码,因为代码库不会被无处不在的 Eithers、Maybes、空集合等弄得乱七八糟。

    这不一定是真的。任何一部分。

    异常问题

    异常的问题在于它们规避了正常的控制流,这会使代码难以推理。这看起来如此明显以至于几乎不值得一提,直到你最终在调用堆栈深处抛出 20 个调用的错误,其中不清楚首先触发错误的是什么:即使堆栈跟踪可能指向你到代码中的确切行,您可能很难弄清楚申请状态导致错误发生。事实上,在命令式/过程式程序中,您可能对状态转换没有纪律,这当然是 FP 试图解决的全部问题。

    也许,也许不是:它可能是任何一个

    你不应该在整个代码库中无处不在的 Maybes/Eithers,并且出于同样的原因,你不应该在整个代码库中随心所欲地抛出异常:它会使代码过于复杂。您应该拥有作为系统入口点的文件,并且那些与 I/O 有关的文件将充满 Maybes/Eithers,但他们应该委托给普通的根据语言(您未指定语言)通过其他机制提升或分派的功能。至少具有选项类型的语言几乎总是支持一流的功能,你总是可以使用回调。

    这有点像可测试性作为代码质量的代表:如果你的代码很难测试它大概存在结构性问题。如果你的代码库在每个文件中都充满了 Maybes/Eithers大概存在结构性问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多