使用 Dune 的库

先决条件

简介

Dune 提供了多种方法将模块组织成库。我们将了解 Dune 用于构建包含模块的库的项目结构机制。

本教程使用 Dune 构建工具。请确保你已安装 3.7 或更高版本。

最小项目设置

本节详细介绍了几乎最小的 Dune 项目设置结构。查看 你的第一个 OCaml 程序,了解使用 dune init proj 命令自动进行设置。

$ mkdir mixtli; cd mixtli

在这个目录中,创建另外四个文件:dune-projectdunecloud.mlwmo.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 (Ns)
Cumulonimbus (Cb)

以下是目录内容

$ 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 模块:StratusCumulus

以下是不同的名称

  • 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.mlilib/status.mli。之后,在“包含子目录”部分中,lib/cumulus/m.mlilib/status/m.mli 文件也是相同的。

以下是一种使用命名模块类型(也称为签名)修复此问题的可能方法。首先,删除 lib/cumulus/m.mlilib/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.MStratus.M 被明确绑定到相同的接口,该接口定义在模块 Wmo 中。

禁用库包装

本节详细介绍了 Dune 如何将库的内容包装到专用模块中。它还展示了如何禁用此机制。

lib 文件夹的内容被裁剪回接近于“”部分中的状态。删除文件 lib/cumulus/m.mllib/stratus/m.mllib/wmo.mlilib/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 是包装模块,它包含 CumulusStratus 作为子模块。文件 lib/cumulus.mllib/stratus.ml 的编译分别生成模块 Wmo__CumulusWmo__StratusWmo 的前子模块分别是后者的别名。

包装器 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))

在这种情况下,“库”仅包含 CumulusStratus 模块,它们捆绑在一起,并排放置。在 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 提供了几种方法将模块排列到库中。

仍然需要帮助吗?

帮助改进我们的文档

所有 OCaml 文档都是开源的。看到错误或不清楚的地方吗?提交一个拉取请求。

OCaml

创新、社区、安全。