发布 Haskell 库时如何确定合理的包依赖范围?

How do I determine reasonable package dependency bounds when releasing a Haskell library?

在 Hackage 上发布库时,如何确定依赖项的合理范围?

这是一个非常简短的问题 - 不确定我可以提供哪些额外信息。

了解根据使用的是 stack 还是 cabal 是否有不同的处理方式也会很有帮助。


基本上我的问题与当前设置为的阴谋集团约束有关:

library
  hs-source-dirs: src
  default-language: Haskell2010
  exposed-modules: Data.ByteUnits
  build-depends:       base >=4.9 && <4.10
                       , safe == 0.3.15

我认为 == 不是个好主意。

边界的目的是确保您使用的依赖项版本具有您需要的功能。有一些最早的版本 X 引入了所有这些功能,因此您需要一个至少为 X 的下限。 可能 要求的功能已从更高版本 删除 Y,在这种情况下,您需要指定一个上限小于 Y:

build-depends:    foo >= X && < Y

理想情况下,永远不会删除您需要的功能,在这种情况下,您可以放弃上限。这意味着仅当您知道您的功能从更高版本中消失时才需要上限。否则,假设 foo >= X 就足够了,直到你有相反的证据。

foo == X 应该很少使用;它基本上是 foo >= X && <= X 的缩写,并说明您正在使用 X 版本中 的功能;它不在早期版本中,并且在更高版本中被删除。如果您发现自己处于这种情况,最好尝试重写您的代码以不再依赖该功能,这样您就可以 return 使用 foo >= Z (通过放宽对确切地说是 X 版本,您可以使用 foo 的更早版本 Z < X)。

这是一个棘手的问题,因为社区中对最佳实践有不同的看法,并且在确定边界的容易程度和提供与依赖项版本的最大兼容性之间需要权衡取舍。在我看来,基本上可以采用三种方法:

  1. 查看您当前使用的依赖项的版本,例如safe-0.3.15。假设该包遵循 PVP,不会发布 0.4 版本之前的重大更改,并添加:safe >= 0.3.15 && < 0.4
  2. 以上内容不错,但限制了许多可能有效的构建计划。您可以花时间针对其他版本的依赖项进行测试。例如,如果您针对 0.2.12 和 0.4.3 进行测试并且它们似乎都有效,您可能希望扩展到 safe >= 0.2.12 && < 0.5
    • 注意:一个常见的错误是在你的包的未来版本中,你忘记检查与旧版本的兼容性,结果你正在使用safe-0.4.1 中引入的新功能,使旧边界无效。不幸的是,没有多少自动化工具可以检查这一点。
  3. 忘掉所有这些:完全没有版本限制,并让包的使用者负责确保构建计划中的兼容性。这样做的缺点是可能会创建无效的构建计划,但好处是您的边界不会消除可能好的构建计划。 (这基本上是假阳性与假阴性的权衡。)

Stackage project 每晚运行构建,通常可以让您知道您的包何时被新版本的依赖项破坏,并通过提供已知的预构建快照让用户更容易使用您的包上班。这对情况 (3) 尤其有帮助,对 (2) 中的宽松下限也有一点帮助。

您可能还想考虑使用 Travis 配置针对旧的 Stackage 快照进行测试,例如https://github.com/commercialhaskell/stack/blob/master/doc/travis-complex.yml

我想您知道 Haskell Package Versioning Policy (PVP)。这提供了一些指导,既隐含在它分配给版本的前三个组件 ("A.B.C") 的含义中,又加上一些关于 Cabal 版本范围的明确建议。

粗略地说,具有相同 "A.B" 的未来版本不会引入任何重大更改(包括引入可能改变其他代码行为的孤立实例),但可能会添加新的绑定、类型等. 前提是你只使用了合格的导入或明确的导入列表:

import qualified Something as S
import Something (foo, bar)

您可以安全地编写以下形式的依赖项:

something >= 1.2.0 && < 1.6

假设您已经通过 1.5.6 测试了 1.2.0,并且您有信心它将继续 运行 所有未来 1.5.xs(非破坏性变化)但可以想象在未来 1.6.

如果你导入了一个不合格的包(如果你要重新导出它的一大块 API,你可能会这样做),你需要一个变体:

the-package >= 1.2.0 && < 1.5.4   -- tested up to 1.5.3 API
the-package >= 1.5.3 && < 1.5.4   -- actually, this API precisely

如果您定义一个孤儿实例,还有一个警告(参见 PVP)。

最后,当导入一些简单、稳定的包时,您只导入了最明显稳定的组件,您可能会做出以下假设:

the-package >= 1.2.0 && < 2

会很安全。

查看大型、复杂、编写良好的程序包的 Cabal 文件可能会让您对实践中所做的事情有所了解。例如,lens 包广泛使用以下形式的依赖项:

array >= 0.3.0.2 && < 0.6

但偶尔会有依赖性,例如:

free >= 4 && < 6

(很多情况下,这些更广泛的依赖是同一个作者写的包,他显然可以保证不破坏自己的包,所以可以松懈一点。)

一个“万无一失”的答案是:允许那些您确信会成功运行的版本!如果你只用 safe-0.3.15 编译过你的项目,那么从技术上讲你不知道它是否也适用于 safe-0.3.15,因此 cabal 提供的约束是正确的。如果您想与其他版本兼容,请通过连续向后测试它们。这可以通过完全禁用 .cabal 文件中的约束然后执行

来最简单地完成
$ cabal configure --constraint='safe==XYZ' && cabal test

对于每个版本 XYZ = 0.3.14 等..

实际上,这有点偏执。特别是,包遵循 Package Versioning Policy 是一种很好的礼仪,它要求新的 次要版本 永远不会破坏任何构建。也就是说,如果 0.3.15 有效,那么 0.3.16 等无论如何也应该有效。因此,如果您只检查了 0.3.15,则保守约束实际上是 safe >=0.3.15 && <0.4。可能 safe >=0.3 && <0.4 也是安全的 。 PVP 还要求您不要使用比您确认工作更宽松的 主要 版本界限,即它强制执行 <0.4 约束。

通常,这仍然是不必要的严格。这取决于您使用某些包的紧密程度。特别是,有时您需要显式依赖一个包,只是为了获得更重要的依赖项所使用的类型的一些额外配置功能。在这种情况下,我倾向于完全不为辅助包设置任何界限。举一个极端的例子,如果我依赖 diagrams-lib,那么就没有充分的理由给 diagrams-core 任何界限,因为它无论如何都与 diagrams-lib.

相关联

对于像 containers 这样非常稳定和标准的软件包,我通常也不会为边界烦恼。例外当然是 base.


选择safe包作为例子吗?