简体   繁体   English

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

[英]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). 如果有完全不同的方法可以满足这些条件,请分享并说明它比替代方法更好(请参阅最后的粗略想法)。

  1. Each application must specify its own configuration signature/type. 每个应用程序必须指定自己的配置签名/类型。
  2. Each application may only be constructed using an instantiation of the specified configuration. 只能使用指定配置的实例来构造每个应用程序。
  3. Configuration construction is handled at the call sites however the caller chooses, so long as the result satisfies the application's specification. 只要呼叫结果可以满足应用程序的规范,就可以在呼叫站点处进行配置构造,但呼叫者可以选择。
  4. The signatures of each application's 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

Parts that compile 编译部分

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

Where I'm stuck 我被困在哪里

Goal : make and launch a DemoApp from a toplevel let () = ... . 目标 :从顶层let () = ... makelaunch 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_configDemoApp 内部成功构造了一个。)

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

Attempt 1 尝试1

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域位于不同的模块中。

Attempt 2 尝试2

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失败。

Attempt 3 尝试3

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: 重新定义DemoAppconfig类型以使用它:

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.configdemo_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.configAPPLICATION.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类型之外 ,是否有办法强制执行APPLICATIONDemoApp施加的所有约束?

If this is a dead end 如果这是死胡同

...here are some other strategies I haven't experimented with enough yet, but might work: ...还有一些我还没有尝试过的其他策略,但是可能有效:

  1. Functors : each 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. 然后,他们创建该配置的一个实例,并将其传递给专用应用程序结构的构造函数,以创建其配置的应用程序。
  2. First-class modules : 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). 应用程序构造函数的调用者还必须具有一种构造满足配置签名的模块的方法(因此,可以定义具体的配置设置)。
  3. Row polymorphism : I've read a little about how the object system supports row polymorphism. 行多态性 :我已经阅读了一些有关对象系统如何支持行多态性的知识。 Maybe each application's 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 . 这意味着tconfig是抽象的(并且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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM