haskell - 使用类型类在 Haskell 应用程序中实现依赖倒置?

标签 haskell dependencies clean-architecture dependency-inversion

设计大型应用程序时的一个主要架构目标是减少耦合和依赖性。我所说的依赖性是指源代码依赖性,当一个函数或数据类型使用另一个函数或另一种类型时。高级架构指南似乎是 Ports & Adapters体系结构,略有不同也称为 Onion Architecture , Hexagonal Architecture , 或 Clean Architecture : 为应用程序的建模的类型和函数位于中心,然后是在域的基础上提供有用服务的用例,在最外圈是持久性、网络和 UI 等技术方面。

依赖规则 说依赖必须只指向内部。例如。;持久性可能取决于用例中的功能和类型,而用例可能取决于域中的功能和类型。但是不允许域依赖于外环。我应该如何在 Haskell 中实现这种架构?具体来说:我如何实现一个不依赖(=导入)持久性模块中的函数和类型的用例模块,即使它需要检索和存储数据?

假设我想通过函数U.placeOrder::D.Customer -> [D.LineItem] -> IO U.OrderPlacementResult 实现一个用例订单放置>,它从行项目创建订单并尝试保留顺序。这里,U 表示用例模块,D 表示领域模块。该函数返回一个 IO 操作,因为它以某种方式需要保留顺序。然而,持久性本身位于最外层的架构环中——在某个模块 P 中实现;因此,上述函数不能依赖于从 P 导出的任何内容。

我可以想象两种通用的解决方案:

  1. 高阶函数:U.placeOrder 函数接受一个额外的函数参数,比如 U.OrderDto -> U.PersistenceResult。该功能在持久化(P)模块中实现,但依赖于U模块的类型,而U模块则不需要声明对 P 的依赖。
  2. 类型类:U模块定义了一个声明上述功能的Persistence类型类。 P 模块依赖于此类型类并为其提供实例。

变体 1 非常明确但不是很笼统。它可能会导致具有许多参数的函数。变体 2 不那么冗长(参见,例如,here)。然而,变体 2 导致许多无原则类型类,这在大多数现代 Haskell 教科书和教程中被认为是不好的做法。

所以,我有两个问题:

  • 我是否缺少其他选择?
  • 如果有的话,一般推荐哪种方法?

最佳答案

确实还有其他选择(见下文)。

虽然您可以使用partial application as dependency injection , 我不认为它是合适的 functional architecture ,因为它让一切变得不纯。

对于您当前的示例,它似乎并不太重要,因为 U.placeOrder 已经是不纯的,但一般来说,您希望您的 Haskell 代码包含尽可能多的引用尽可能透明的代码。

您有时会看到涉及 Reader monad 的建议,其中“依赖项”作为读取器上下文而不是直接函数参数传递给函数,但据我所知,这些只是(同构?)相同想法的变体,具有相同的问题。

更好的选择是功能核心、命令式 shell免费 monads。可能还有其他选择,但这些是我所知道的。

函数式核心,命令式外壳

您通常可以分解代码,以便将域模型定义为一组纯函数。使用 Haskell 和 F# 等语言通常更容易做到这一点,因为您可以使用求和类型来传达决策。例如,U.placeOrder 函数可能如下所示:

U.placeOrder :: D.Customer -> [D.LineItem] -> U.OrderPlacementDecision

请注意,这是一个纯函数,其中 U.OrderPlacementDecision 可能是一个枚举用例所有可能结果的总和类型。

那是您的功能核心。然后,您将在 impureim sandwich 中编写您的命令式 shell(例如您的 main 函数) :

main :: IO ()
main = do
  stuffFromDb <- -- call the persistence module code here
  customer -- initialised from persistence module, or some other place
  lineItems -- ditto
  let decision = U.placeOrder customer lineItems
  _ <- persist decision
  return ()

(我显然没有尝试对代码进行类型检查,但我希望它足够正确,以便理解要点。)

自由单子(monad)

功能核心,命令式外壳 是迄今为止实现所需架构结果的最简单方法,而且显然通常可以逃脱。不过,在某些情况下这是不可能的。在这些情况下,您可以改用免费的 monad。

使用免费的 monad,您可以定义大致等同于面向对象接口(interface)的数据结构。就像在功能核心、命令式 shell 案例中一样,这些数据结构是求和类型,这意味着您可以保持函数的纯净。然后,您可以在生成的表达式树上运行一个不纯的解释器。

我写了an article series关于如何考虑 F# 和 Haskell 中的依赖注入(inject)。我最近还发表了 an article那(除其他外)展示了这种技术。我的大部分文章都附有 GitHub 存储库。

关于haskell - 使用类型类在 Haskell 应用程序中实现依赖倒置?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61794525/

相关文章:

java - 我是否需要为 Java 中的所有不可变变量指定 «final» 修饰符?

java - 我无法理解鲍勃叔叔书中的整洁架构部分(MVP)

android - MVVM 整洁架构中是否真的需要 Data Mapper 层?

haskell - 无法安装Polyparse库

Haskell String-> Int 类型转换

haskell - haskell 中最快的错误单子(monad)是什么?

dependencies - 添加对 Chef Cookbook 的依赖项

dependencies - automake 中的 header 依赖项

java - 如何解决未解析的依赖关系 ':app@debug/compileClasspath' : Could not resolve com. google.android.gms :play-services-base:[15. 0.0, 16.0.0)

ubuntu - ubuntu : how to make work an haskell build system? 上的 Sublime Text 3