OCaml:使用模块签名、类型或其他方式解耦

OCaml: decoupling using module signatures, types, or something else

我正在用 OCaml 开发应用程序,预计代码库最终会生成多个可执行文件。每个应用程序的配置选项也将发生变化。

我对 OCaml 的模块系统比较陌生,但我已经尝试使用它们来强制执行我想要的结构。如果有满足这些标准的完全不同的方法,请分享并解释它比其他方法更好的地方(请参阅最后的粗略想法)。

  1. 每个应用程序必须指定自己的配置 signature/type。
  2. 每个应用程序只能使用指定配置的实例来构建。
  3. 只要结果满足应用程序的规范,无论调用者如何选择,配置构造都会在调用点处理。
  4. 每个应用程序的 t 类型(及其接受 t 的所有函数)的签名不得更改,即使其配置规范发生更改。但是,可以访问 t 的应用程序功能会自动访问配置规范提供的所有内容。

一切都使用

测试
$ ocaml --version
The OCaml toplevel, version 4.03.0
$ ocamlbuild --version
ocamlbuild 0.9.2
$ ocamlbuild sandbox.byte

编译部分

一个 APPLICATION 是一个 CONFIGURABLE_SYSTEM 仅通过使用一些内部 config 类型构造的:

module type CONFIGURABLE_SYSTEM = sig
  type t
  type config

  val make : config -> t
end

module type APPLICATION = sig
  include CONFIGURABLE_SYSTEM

  val launch : t -> unit
end

这是一个满足上述签名的演示应用程序:

module DemoApp : APPLICATION = struct
  type config = { color : color_scheme }
   and color_scheme = | HighContrast | Chromatic

  type t = (config * string)

  let make cfg = (cfg, "default string state")
  let launch (cfg, _str) =
    match cfg.color with
    | HighContrast -> print_string "Using high contrast settings...\n"
    | Chromatic -> print_string "Using chromatic settings...\n"

  let default_config = { color = HighContrast }
end

我被困在哪里

目标makelaunch 来自顶层的 DemoApp let () = ....

问题:从定义它的 DemoApp 模块外部构造一个 config 记录。 (注意 let default_config 内部 DemoApp 成功构造了一个。)

let () =
  let config = (*** What goes here? ***)
  let demo = DemoApp.make config in
  DemoApp.launch demo

尝试 1

尝试简单明了的方法:

let () =
  let config = { color = HighContrast } in
  ()

Error: unbound record field color 失败。这并不奇怪,因为 color 字段在不同的模块中。

尝试 2

创建一个临时模块,在其中打开 DemoApp,在那里创建一个 config 实例,然后从外部获取它:

let () =
  let config =
    let module M = struct
        open DemoApp
        let config = { color = HighContrast }
      end
    in
    M.config
  in
  ()

令人惊讶的是,这也失败了Error: unbound record field color

尝试 3

咬紧牙关,定义一个只能用作 DemoApp 配置类型的顶级类型。

type demo_config =
  {
    color : color_scheme
  }
 and color_scheme = | HighContrast | Chromatic

重新定义 DemoAppconfig 类型以使用它:

Module DemoApp : APPLICATION = struct
  type config = demo_config
  ... (* everything else is the same as above *)
end

...试一试:

let () =
  let cfg = { color = HighContrast } in
  let demo = DemoApp.make cfg in
  ()

Error: This expression has type demo_config but an expression was expected of type DemoApp.config

哎呀。模块系统不识别 DemoApp.configdemo_config,即使前者被定义为后者。这是因为 CONFIGURABLE_SYSTEM.configAPPLICATION.config 是抽象的,而 DemoApp 产生一个 APPLICATION,意味着它的 config 定义被隐藏了吗?如果是这样,是否有办法强制执行 APPLICATIONDemoApp 除了 隐藏具体 config 类型施加的所有约束?

如果这是死胡同

...这里有一些我还没有充分试验过但可能有效的其他策略:

  1. Functors:每个 APPLICATION 结构,如 DemoApp,由模块参数化,最好是签名,代表配置规范。调用者定义自己的模块结构以满足应用程序的配置规范,使用它来创建自己的专用应用程序结构。然后他们创建配置的实例,并将其传递给专用应用程序结构的构造函数以创建他们配置的应用程序。
  2. First-class modules: make 接受任何满足配置签名的模块。此签名在 CONFIGURABLE_SYSTEM 中必须是抽象的,并由每个 APPLICATION 结构具体定义。应用程序构造函数的调用者还必须有一种方法来构造满足配置签名的模块(并因此定义具体的配置设置)。
  3. 行多态性:我已经阅读了一些有关对象系统如何支持行多态性的内容。也许每个应用程序的 config 类型都可以定义为行多态对象签名,并且 make 调用者可以传递至少具有该签名中定义的字段的任何对象。我对这种方法不那么兴奋,因为它可以鼓励跨所有应用程序的通用配置,并导致各种应用程序配置之间奇怪的 naming/interpretation 冲突(例如,app1 的配置需要一个 x 字段,这是一个 int 和 app2 的配置期望 x : string,或者更糟的是他们期望相同的 names/types 但解释不同)。

您的问题在这里:

module DemoApp : APPLICATION = struct

通过此模块类型注释,您可以限制 DemoApp 的类型。这意味着 tconfig 是抽象的(并且 colorscheme 是隐藏的),因为它们在 APPLICATION 中是这样声明的。

您要确保 DemoApp 遵守 APPLICATION 签名,同时仍公开实际数据类型。您可以简单地删除注释,它会正常工作。

.mli 中,你会得到类似的东西:

module DemoApp : sig
  type config = { color : color_scheme }
   and color_scheme = | HighContrast | Chromatic
  type t = (config * string)

  (* include APPLICATION while style exposing the concrete types. *)
  include APPLICATION with type t := t and type config := config
end

请注意,您想要的名称为 "transparent ascription"。正如您所发现的,通常的模块类型归属隐藏了模块中类型的实现(它是 "opaque")。透明归属不会。