OCaml:使用模块签名、类型或其他方式解耦
OCaml: decoupling using module signatures, types, or something else
我正在用 OCaml 开发应用程序,预计代码库最终会生成多个可执行文件。每个应用程序的配置选项也将发生变化。
我对 OCaml 的模块系统比较陌生,但我已经尝试使用它们来强制执行我想要的结构。如果有满足这些标准的完全不同的方法,请分享并解释它比其他方法更好的地方(请参阅最后的粗略想法)。
- 每个应用程序必须指定自己的配置 signature/type。
- 每个应用程序只能使用指定配置的实例来构建。
- 只要结果满足应用程序的规范,无论调用者如何选择,配置构造都会在调用点处理。
- 每个应用程序的
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
我被困在哪里
目标:make
和 launch
来自顶层的 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
重新定义 DemoApp
的 config
类型以使用它:
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.config
和 demo_config
,即使前者被定义为后者。这是因为 CONFIGURABLE_SYSTEM.config
和 APPLICATION.config
是抽象的,而 DemoApp
产生一个 APPLICATION
,意味着它的 config
定义被隐藏了吗?如果是这样,是否有办法强制执行 APPLICATION
对 DemoApp
、 除了 隐藏具体 config
类型施加的所有约束?
如果这是死胡同
...这里有一些我还没有充分试验过但可能有效的其他策略:
- Functors:每个
APPLICATION
结构,如 DemoApp
,由模块参数化,最好是签名,代表配置规范。调用者定义自己的模块结构以满足应用程序的配置规范,使用它来创建自己的专用应用程序结构。然后他们创建配置的实例,并将其传递给专用应用程序结构的构造函数以创建他们配置的应用程序。
- First-class modules:
make
接受任何满足配置签名的模块。此签名在 CONFIGURABLE_SYSTEM
中必须是抽象的,并由每个 APPLICATION
结构具体定义。应用程序构造函数的调用者还必须有一种方法来构造满足配置签名的模块(并因此定义具体的配置设置)。
- 行多态性:我已经阅读了一些有关对象系统如何支持行多态性的内容。也许每个应用程序的
config
类型都可以定义为行多态对象签名,并且 make
调用者可以传递至少具有该签名中定义的字段的任何对象。我对这种方法不那么兴奋,因为它可以鼓励跨所有应用程序的通用配置,并导致各种应用程序配置之间奇怪的 naming/interpretation 冲突(例如,app1 的配置需要一个 x
字段,这是一个 int
和 app2 的配置期望 x : string
,或者更糟的是他们期望相同的 names/types 但解释不同)。
您的问题在这里:
module DemoApp : APPLICATION = struct
通过此模块类型注释,您可以限制 DemoApp
的类型。这意味着 t
和 config
是抽象的(并且 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")。透明归属不会。
我正在用 OCaml 开发应用程序,预计代码库最终会生成多个可执行文件。每个应用程序的配置选项也将发生变化。
我对 OCaml 的模块系统比较陌生,但我已经尝试使用它们来强制执行我想要的结构。如果有满足这些标准的完全不同的方法,请分享并解释它比其他方法更好的地方(请参阅最后的粗略想法)。
- 每个应用程序必须指定自己的配置 signature/type。
- 每个应用程序只能使用指定配置的实例来构建。
- 只要结果满足应用程序的规范,无论调用者如何选择,配置构造都会在调用点处理。
- 每个应用程序的
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
我被困在哪里
目标:make
和 launch
来自顶层的 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
重新定义 DemoApp
的 config
类型以使用它:
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.config
和 demo_config
,即使前者被定义为后者。这是因为 CONFIGURABLE_SYSTEM.config
和 APPLICATION.config
是抽象的,而 DemoApp
产生一个 APPLICATION
,意味着它的 config
定义被隐藏了吗?如果是这样,是否有办法强制执行 APPLICATION
对 DemoApp
、 除了 隐藏具体 config
类型施加的所有约束?
如果这是死胡同
...这里有一些我还没有充分试验过但可能有效的其他策略:
- Functors:每个
APPLICATION
结构,如DemoApp
,由模块参数化,最好是签名,代表配置规范。调用者定义自己的模块结构以满足应用程序的配置规范,使用它来创建自己的专用应用程序结构。然后他们创建配置的实例,并将其传递给专用应用程序结构的构造函数以创建他们配置的应用程序。 - First-class modules:
make
接受任何满足配置签名的模块。此签名在CONFIGURABLE_SYSTEM
中必须是抽象的,并由每个APPLICATION
结构具体定义。应用程序构造函数的调用者还必须有一种方法来构造满足配置签名的模块(并因此定义具体的配置设置)。 - 行多态性:我已经阅读了一些有关对象系统如何支持行多态性的内容。也许每个应用程序的
config
类型都可以定义为行多态对象签名,并且make
调用者可以传递至少具有该签名中定义的字段的任何对象。我对这种方法不那么兴奋,因为它可以鼓励跨所有应用程序的通用配置,并导致各种应用程序配置之间奇怪的 naming/interpretation 冲突(例如,app1 的配置需要一个x
字段,这是一个int
和 app2 的配置期望x : string
,或者更糟的是他们期望相同的 names/types 但解释不同)。
您的问题在这里:
module DemoApp : APPLICATION = struct
通过此模块类型注释,您可以限制 DemoApp
的类型。这意味着 t
和 config
是抽象的(并且 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")。透明归属不会。