如何在Cabal项目中合理组织公共与私有库?
项目背景与目标
我有一个多模块Haskell项目,情况如下:
- 2个对外导出的公共库模块:
A和B(B是后端模块,被A调用,但因其他项目可能用到B,所以选择同时导出两者); - 其余为内部模块;
- 还有一个包含
main :: IO ()的Main.hs,对应executablestanza,目前用于验证B的导出实体,后续会替换为测试代码。
希望实现以下目标:
- 每个文件对应一个模块,且每个模块对应独立的
librarystanza; - 每个
library的build-depends仅包含该模块import语句所需的依赖,数量最少; - 导入模块时,
import Foo.Bar.Baz格式无重复内容。
当前尝试的目录结构
最初的方案是给每个模块设置独立目录、独立的library stanza和build-depends列表。目前尝试的结构如下:
. | +--- lib/ | | | +--- A/ | | | | | +--- A.hs | | | | | +--- Internal/ | | | | | +--- E.hs | +--- B/ | | | | | +--- B.hs | | | +--- C/ | | | | | +--- C.hs | | | +--- D/ | | | | | +--- D.hs | +--- exe/ | | | +--- Main.hs | +--- myproject.cabal
结构说明
E.hs是A的实现细节,放在A/Internal目录下,导入方式为:import A.Internal.E- 公共模块
A和B的导入方式符合定位:import A import B - 若
C是B的实现细节,调整结构如下,此时每个模块仍有独立目录,可各自配置build-depends:. | +--- lib/ | | | +--- A/ | | | | | +--- A.hs | | | | | +--- Internal/ | | | | | +--- E.hs | +--- B/ | | | | | +--- B.hs | | | | | +--- Internal/ | | | | | +--- C.hs | +--- D/ | | | +--- D.hs | +--- exe/ | | | +--- Main.hs | +--- myproject.cabal
遇到的困境
当D也作为A的实现细节时,若把D.hs放在A/Internal目录下:
. | +--- lib/ | | | +--- A/ | | | | | +--- A.hs | | | | | +--- Internal/ | | | | | +--- E.hs | | | | | +--- D.hs ...
此时E.hs和D.hs处于同一目录,无法为它们设置独立的library stanza及各自的build-depends。
若为每个内部模块添加一层目录:
. | +--- lib/ | | | +--- A/ | | | | | +--- A.hs | | | | | +--- Internal/ | | | | | +--- E/ | | | | | | | +--- E.hs | | | | | +--- D/ | | | | | +--- D.hs ...
会导致导入时出现重复命名:
import A.Internal.D.D import A.Internal.E.E
这种写法不够简洁美观。
可行的目录组织方案
方案1:利用cabal配置拆分library
不需要给每个内部模块单独建目录,而是通过cabal的hs-source-dirs和exposed-modules为每个模块创建独立的library stanza,精准指定源文件和依赖。
例如,在myproject.cabal中配置:
library a-internal-e hs-source-dirs: lib/A/Internal exposed-modules: A.Internal.E build-depends: base >=4.14 && <5 -- 仅添加E.hs所需的依赖 library a-internal-d hs-source-dirs: lib/A/Internal exposed-modules: A.Internal.D build-depends: base >=4.14 && <5 -- 仅添加D.hs所需的依赖 library A hs-source-dirs: lib/A exposed-modules: A build-depends: base >=4.14 && <5, a-internal-e, a-internal-d
这种方式下,E.hs和D.hs仍在A/Internal目录下,导入路径保持import A.Internal.E和import A.Internal.D,无重复内容,同时每个模块拥有独立的依赖配置。
方案2:扁平化内部模块目录,调整命名前缀
将所有内部模块放在统一的internal目录下,通过模块名前缀区分所属公共库,目录结构如下:
. | +--- lib/ | | | +--- A/ | | | | | +--- A.hs | +--- B/ | | | | | +--- B.hs | +--- internal/ | | | | | +--- A_E.hs | | | | | +--- A_D.hs | | | | | +--- B_C.hs | +--- exe/ | | | +--- Main.hs | +--- myproject.cabal
对应的模块名设为Internal.A.E、Internal.A.D、Internal.B.C,导入时写:
import Internal.A.E import Internal.A.D
每个内部模块仍可配置独立的library stanza,按需设置build-depends,同时避免了导入路径重复。
方案3:独立子项目拆分
将每个模块(包括公共库和内部模块)作为独立子项目,每个子项目有自己的.cabal文件,目录结构如下:
. | +--- A/ | | | +--- src/ | | | | | +--- A.hs | | | +--- A.cabal | +--- A-Internal-E/ | | | +--- src/ | | | | | +--- A/ | | | | | | | +--- Internal/ | | | | | | | +--- E.hs | | | +--- A-Internal-E.cabal | +--- A-Internal-D/ | | | +--- src/ | | | | | +--- A/ | | | | | | | +--- Internal/ | | | | | | | +--- D.hs | | | +--- A-Internal-D.cabal | +--- B/ | | | +--- src/ | | | | | +--- B.hs | | | +--- B.cabal | +--- B-Internal-C/ | | | +--- src/ | | | | | +--- B/ | | | | | | | +--- Internal/ | | | | | | | +--- C.hs | | | +--- B-Internal-C.cabal | +--- exe/ | | | +--- Main.hs | | | +--- exe.cabal | +--- cabal.project
在cabal.project中声明所有子项目:
packages: ./A ./A-Internal-E ./A-Internal-D ./B ./B-Internal-C ./exe
这种方式下每个模块完全独立,build-depends可精准控制,导入路径无重复,但目录结构更分散,适合模块独立性极强的场景。
内容的提问来源于stack exchange,提问作者Enlico

