使用 Dune 的库
简介
Dune 提供了多种方法将模块组织成库。我们将了解 Dune 用于构建包含模块的库的项目结构机制。
本教程使用 Dune 构建工具。请确保你已安装 3.7 或更高版本。
最小项目设置
本节详细介绍了几乎最小的 Dune 项目设置结构。查看 你的第一个 OCaml 程序,了解使用 dune init proj
命令自动进行设置。
$ mkdir mixtli; cd mixtli
在这个目录中,创建另外四个文件:dune-project
、dune
、cloud.ml
和 wmo.ml
。
dune-project
(lang dune 3.7)
(package (name wmo-clouds))
此文件包含全局项目配置。它几乎是最小化的,包括 lang dune
节点,该节点指定所需的 Dune 版本,以及 package
节点,该节点使本教程更简单。
dune
(executable
(name cloud)
(public_name nube))
每个需要某种构建的目录都必须包含一个 dune
文件。executable
节点表示构建可执行程序。
name cloud
节点表示文件cloud.ml
包含可执行程序。public_name nube
节点表示可执行程序使用名称nube
提供。
wmo.ml
module Stratus = struct
let nimbus = "Nimbostratus (Ns)"
end
module Cumulus = struct
let nimbus = "Cumulonimbus (Cb)"
end
cloud.ml
let () =
Wmo.Stratus.nimbus |> print_endline;
Wmo.Cumulus.nimbus |> print_endline
以下是生成的输出
$ opam exec -- dune exec nube
Nimbostratus ()
Cumulonimbus ()
以下是目录内容
$ tree
.
├── dune
├── dune-project
├── cloud.ml
└── wmo.ml
Dune 将其创建的文件和源代码副本存储在名为 _build
的目录中。你无需修改此目录中的任何内容。在使用 Git 管理的项目中,应忽略 _build
目录。
$ echo _build >> .gitignore
你也可以配置编辑器或 IDE 来忽略它。
在 OCaml 中,每个 .ml
文件都定义一个模块。在 mixtli
项目中,文件 cloud.ml
定义了 Cloud
模块,文件 wmo.ml
定义了包含两个子模块的 Wmo
模块:Stratus
和 Cumulus
。
以下是不同的名称
mixtli
是项目的名称(在纳瓦特尔语中表示为 云)。cloud.ml
是 OCaml 源代码文件的名称,在dune
文件中称为cloud
。nube
是可执行命令的名称(在西班牙语中表示为 云)。Cloud
是与文件cloud.ml
关联的模块的名称。Wmo
是与文件wmo.ml
关联的模块的名称。wmo-clouds
是此项目构建的包的名称。
dune describe
命令允许查看项目的模块结构。以下是其输出
((root /home/cuihtlauac/caml/mixtli-dune)
(build_context _build/default)
(executables
((names (cloud))
(requires ())
(modules
(((name Wmo)
(impl (_build/default/wmo.ml))
(intf ())
(cmt (_build/default/.cloud.eobjs/byte/wmo.cmt))
(cmti ()))
((name Cloud)
(impl (_build/default/cloud.ml))
(intf ())
(cmt (_build/default/.cloud.eobjs/byte/cloud.cmt))
(cmti ()))))
(include_dirs (_build/default/.cloud.eobjs/byte)))))
库
在 OCaml 中,库是模块的集合。默认情况下,当 Dune 构建库时,它会将捆绑的模块包装到一个模块中。这允许在同一个项目中,不同库中具有相同名称的多个模块。此功能称为 命名空间,用于模块名称。这与模块对定义的作用类似;它们可以避免名称冲突。
Dune 从目录创建库。让我们来看一个例子。这里,lib
目录包含其源代码。这与 Unix 标准不同,在 Unix 标准中,lib
用于存储编译后的库二进制文件。
$ mkdir lib
lib
目录中包含以下源代码文件
lib/dune
(library (name wmo))
lib/cumulus.mli
val nimbus : string
lib/cumulus.ml
let nimbus = "Cumulonimbus (Cb)"
lib/stratus.mli
val nimbus : string
lib/stratus.ml
let nimbus = "Nimbostratus (Ns)"
lib
目录中找到的所有模块都将捆绑到 Wmo
模块中。此模块与我们在 wmo.ml
文件中定义的模块相同。为了避免冗余,我们删除了它。
$ rm wmo.ml
我们更新构建可执行程序的 dune
文件,使其将库用作依赖项。
dune
(executable
(name cloud)
(public_name nube)
(libraries wmo))
观察结果:
- Dune 从
lib
目录的内容创建了Wmo
模块。 - 目录的名称(这里为
lib
)无关紧要。 - 库名称在
dune
文件中显示为小写(wmo
)。- 在定义中,在
lib/dune
中 - 在
dune
中用作依赖项时
- 在定义中,在
库包装模块
默认情况下,当 Dune 将模块捆绑到库中时,它们会自动包装到一个模块中。可以手动编写包装文件。包装文件的名称必须与库的名称相同。
这里,我们正在为上一节中的 wmo
库创建包装文件。
lib/wmo.ml
module Cumulus = Cumulus
module Stratus = Stratus
以下是理解这些模块定义的方法
- 在左侧,
module Cumulus
表示Wmo
模块包含一个名为Cumulus
的子模块。 - 在右侧,
Cumulus
指的是在文件lib/cumulus.ml
中定义的模块。
运行 dune exec nube
可以看到程序的行为与上一节相同。
当库目录包含包装模块(这里为 wmo.ml
)时,它是唯一公开的模块。该目录中所有其他未出现在包装模块中的基于文件的模块都是私有的。
使用包装文件可以实现以下几件事
- 具有不同的公开名称和内部名称,
module CumulusCloud = Cumulus
- 在包装模块中定义值,
let ... =
- 公开函子应用产生的模块,
module StringSet = Set.Make(String)
- 将相同的接口类型应用于多个模块,而无需重复文件
- 通过不列出模块来隐藏模块
包含子目录
默认情况下,Dune 会从与 dune
文件相同的目录中找到的模块构建库,但它不会查看子目录。可以更改此行为。
在这个例子中,我们创建子目录并将文件移到那里。
$ mkdir lib/cumulus lib/stratus
$ mv lib/cumulus.ml lib/cumulus/m.ml
$ mv lib/cumulus.mli lib/cumulus/m.mli
$ mv lib/stratus.ml lib/stratus/m.ml
$ mv lib/stratus.mli lib/stratus/m.mli
使用 include_subdirs
节点更改默认行为。
lib/dune
(include_subdirs qualified)
(library (name wmo))
更新库包装器以公开从子目录创建的模块。
wmo.ml
module Cumulus = Cumulus.M
module Stratus = Stratus.M
运行 dune exec nube
以查看程序的行为是否与前两节相同。
include_subdirs qualified
语句递归生效,但对包含 dune
文件的子目录除外。请参阅 Dune 文档 获取有关此 主题 的 更多 信息。
删除重复接口
在前面的步骤中,接口是重复的。在本教程的“库”部分中,有两个文件是相同的:lib/cumulus.mli
和 lib/status.mli
。之后,在“包含子目录”部分中,lib/cumulus/m.mli
和 lib/status/m.mli
文件也是相同的。
以下是一种使用命名模块类型(也称为签名)修复此问题的可能方法。首先,删除 lib/cumulus/m.mli
和 lib/status/m.mli
文件。然后修改模块 Wmo
的接口和实现。
wmo.mli
module type Nimbus = sig
val nimbus : string
end
module Cumulus : Nimbus
module Stratus : Nimbus
wmo.ml
module type Nimbus = sig
val nimbus : string
end
module Cumulus = Cumulus.M
module Stratus = Stratus.M
结果相同,除了实现 Cumulus.M
和 Stratus.M
被明确绑定到相同的接口,该接口定义在模块 Wmo
中。
禁用库包装
本节详细介绍了 Dune 如何将库的内容包装到专用模块中。它还展示了如何禁用此机制。
lib
文件夹的内容被裁剪回接近于“库”部分中的状态。删除文件 lib/cumulus/m.ml
、lib/stratus/m.ml
、lib/wmo.mli
和 lib/wmo.ml
。以下是我们需要的唯一文件的内容
lib/dune
(library (name wmo))
lib/cumulus.ml
let nimbus = "Cumulonimbus (Cb)"
let altus = "Altocumulus (Ac)"
lib/stratus.ml
let nimbus = "Nimbostratus (Ns)"
在此设置中,运行 dune utop
允许发现可用内容。
# #show Wmo;;
module Wmo : sig module Cumulus = Wmo.Cumulus module Stratus = Wmo.Stratus end
# #show Wmo.Cumulus;;
module Cumulus : sig val nimbus : string val altus : string end
# #show Wmo.Stratus;;
# module Stratus : sig val nimbus : string end
# #show Wmo__Cumulus;;
module Wmo__Cumulus : sig val nimbus : string val altus : string end
# #show Wmo__Stratus;;
# module Stratus : sig val nimbus : string end
定义了五个模块。Wmo
是包装模块,它包含 Cumulus
和 Stratus
作为子模块。文件 lib/cumulus.ml
和 lib/stratus.ml
的编译分别生成模块 Wmo__Cumulus
和 Wmo__Stratus
。Wmo
的前子模块分别是后者的别名。
包装器 Wmo
可以手动编写。这说明了如何限制包装的子模块的接口。
lib/wmo.ml
module Cumulus : sig val nimbus : string end = Cumulus
module Stratus = Stratus
以下是在 dune utop
中的外观
# #show Wmo.Cumulus;;
module Cumulus : sig val nimbus : string end
# #show Wmo__Cumulus;;
module Wmo__Cumulus : sig val nimbus : string val altus : string end
可以在 Dune 的配置中禁用包装。
lib/dune
(library (name wmo) (wrapped false) (modules cumulus stratus))
在这种情况下,“库”仅包含 Cumulus
和 Stratus
模块,它们捆绑在一起,并排放置。在 dune utop
中检查以下内容,两次。一次使用文件 lib/wmo.ml
不变,第二次删除它之后。
# #show Cumulus;;
module Cumulus : sig val nimbus : string val altus : string end
# #show Stratus;;
module Stratus : sig val nimbus : string end
备注:
- 当文件
lib/wmo.ml
存在时,不列出它的modules
语句会阻止它被捆绑到库中 - 当文件
lib/wmo.ml
不存在时,wrapped false
语句会阻止创建Wmo
包装器
结论
OCaml 模块系统允许以多种方式组织项目。Dune 提供了几种方法将模块排列到库中。