[英]OCaml: decoupling using module signatures, types, or something else
I'm developing an application in OCaml and anticipate the codebase will eventually produce multiple executables. 我正在OCaml中开发一个应用程序,并期望代码库最终将产生多个可执行文件。 The configuration options for each application are also expected to change. 每个应用程序的配置选项也有望更改。
I'm relatively new to OCaml's module system, but I've tried using them to enforce the structure I want. 我对OCaml的模块系统比较陌生,但是我尝试使用它们来增强所需的结构。 If there's an entirely different approach that meets these criteria, please share and explain how it's better than alternative approaches (see rough ideas at the end). 如果有完全不同的方法可以满足这些条件,请分享并说明它比替代方法更好(请参阅最后的粗略想法)。
t
type (and all its functions accepting a t
) must not change, even when its configuration specification changes. 每个应用程序的t
类型(及其所有接受t
函数)的签名,即使其配置规范发生变化,也不得更改。 However, application functions with access to a t
automatically have access to everything provided by the configuration specification. 但是,可以访问t
应用程序功能会自动访问配置规范提供的所有内容。 Everything tested using 一切都经过测试
$ ocaml --version
The OCaml toplevel, version 4.03.0
$ ocamlbuild --version
ocamlbuild 0.9.2
$ ocamlbuild sandbox.byte
An APPLICATION
is a CONFIGURABLE_SYSTEM
constructed only by using some internal config
type: APPLICATION
是仅通过使用某些内部config
类型构造的CONFIGURABLE_SYSTEM
:
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
Here's a demo application satisfying the above signatures: 这是一个满足上述签名的演示应用程序:
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
Goal : make
and launch
a DemoApp
from a toplevel let () = ...
. 目标 :从顶层let () = ...
make
并launch
DemoApp
。
Problem : Construct a config
record from outside the DemoApp
module where it's defined. 问题 :从定义它的DemoApp
模块外部构造一个config
记录。 (Notice the let default_config
successfully constructs one from inside DemoApp
.) (请注意, let default_config
从DemoApp
内部成功构造了一个。)
let () =
let config = (*** What goes here? ***)
let demo = DemoApp.make config in
DemoApp.launch demo
Try the obvious approach with minimal ceremony: 尝试用最少的仪式进行显而易见的方法:
let () =
let config = { color = HighContrast } in
()
Fails with Error: unbound record field color
. 失败并发生Error: unbound record field color
。 Not entirely surprising because the color
field is in a different module. 这并不完全令人惊讶,因为color
域位于不同的模块中。
Create an ad-hoc module, open DemoApp
inside it, make a config
instance there, and then grab it from outside: 创建一个临时模块,在其中打开DemoApp
在其中创建一个config
实例,然后从外部抓取它:
let () =
let config =
let module M = struct
open DemoApp
let config = { color = HighContrast }
end
in
M.config
in
()
Surprisingly, this also fails with Error: unbound record field color
. 令人惊讶的是,这也因Error: unbound record field color
失败。
Grit my teeth and define a top-level type that should only ever be used as a DemoApp
config type. 咬紧牙关,定义一个顶级类型,该类型只能用作DemoApp
配置类型。
type demo_config =
{
color : color_scheme
}
and color_scheme = | HighContrast | Chromatic
Redefine DemoApp
's config
type to use it: 重新定义DemoApp
的config
类型以使用它:
Module DemoApp : APPLICATION = struct
type config = demo_config
... (* everything else is the same as above *)
end
...and give it a try: ...并尝试一下:
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 错误:此表达式的类型为demo_config,但应为DemoApp.config类型的表达式
Ouch. 哎哟。 The module system doesn't identify DemoApp.config
with demo_config
even though the former is defined as the latter. 即使将前者定义为后者,模块系统也无法将DemoApp.config
与demo_config
标识。 Is this because the CONFIGURABLE_SYSTEM.config
, and in turn APPLICATION.config
, are abstract and DemoApp
produces an APPLICATION
, meaning its config
definition is hidden? 这是因为CONFIGURABLE_SYSTEM.config
和APPLICATION.config
是抽象的,而DemoApp
生成了APPLICATION
,意味着其config
定义被隐藏了吗? If so, is there a way to enforce all the constraints imposed by APPLICATION
on DemoApp
, except hiding the concrete config
type? 如果是这样, 除了隐藏具体的config
类型之外 ,是否有办法强制执行APPLICATION
对DemoApp
施加的所有约束?
...here are some other strategies I haven't experimented with enough yet, but might work: ...还有一些我还没有尝试过的其他策略,但是可能有效:
APPLICATION
struct, like DemoApp
, is parameterized by a module, ideally a signature, representing the configuration spec. 函数 :像DemoApp
一样,每个APPLICATION
结构都由一个模块参数化,理想情况下是一个签名,代表配置规范。 Callers define their own module structs satisfying the application's config spec use it to create their own specialized application struct. 调用者定义自己的模块结构,以满足应用程序的配置规范,并使用它来创建自己的专用应用程序结构。 Then they make an instance of the config, and pass it to the specialized application struct's constructor to make their configured application. 然后,他们创建该配置的一个实例,并将其传递给专用应用程序结构的构造函数,以创建其配置的应用程序。 make
accepts any module satisfying a configuration signature. 一流的模块 : make
接受任何满足配置签名的模块。 This signature must be abstract in the CONFIGURABLE_SYSTEM
, and concretely defined by each APPLICATION
struct. 此签名必须在CONFIGURABLE_SYSTEM
是抽象的,并且由每个APPLICATION
结构具体定义。 Callers of the application constructor must also have a way to construct a module satisfying the configuration signature (and consequently, defining concrete configuration settings). 应用程序构造函数的调用者还必须具有一种构造满足配置签名的模块的方法(因此,可以定义具体的配置设置)。 config
type could be defined as a row polymorphic object signature, and make
callers could pass any object which has at least the fields defined in that signature. 也许每个应用程序的config
类型都可以定义为行多态对象签名,并且make
调用者可以传递至少具有该签名中定义的字段的任何对象。 I'm less thrilled about this approach because it could encourage a universal configuration across all applications, and lead to weird naming/interpretation collisions across the various application configurations (eg app1's config demands an x
field that's an int
and app2's config expects an x : string
, or worse they expect the same names/types but interpret them differently). 我对这种方法并不感到兴奋,因为它可以鼓励所有应用程序之间进行通用配置,并导致各种应用程序配置之间发生奇怪的命名/解释冲突(例如,app1的配置要求x
字段为int
而app2的配置要求x : string
,或更糟糕的是,他们期望使用相同的名称/类型,但对它们的解释有所不同)。 Your issue is here: 您的问题在这里:
module DemoApp : APPLICATION = struct
By this module type annotation, you constraint the type of DemoApp
. 通过此模块类型注释,可以约束DemoApp
的类型。 This means that t
and config
are abstract (and that colorscheme
is hidden), since that's how they are declared in APPLICATION
. 这意味着t
和config
是抽象的(并且colorscheme
是隐藏的),因为这是在APPLICATION
中声明的方式。
You want to ensure that DemoApp
respects the APPLICATION
signature while still exposing the actual datatype. 您想要确保DemoApp
在仍然暴露实际数据类型的同时尊重APPLICATION
签名。 You can simply remove the annotation, and it'll work fine. 您只需删除注释即可,它将正常工作。
In the .mli
, you would have something like that : 在.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
Note that what you desire is named "transparent ascription". 请注意,您想要的名称被称为“透明名称”。 As you discovered, the usual module type ascription hide the implementation of types in the module (it's "opaque"). 如您所见,通常的模块类型归属隐藏了模块中类型的实现(“不透明”)。 A transparent ascription would not. 透明的名称不会。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.