我有这种OO情况我试图在Ocaml中实现:
两个类X1
和X2
,都是子类型X
(X1 <: X
和X2 <: X
),我想编写一个动态返回X
的函数这将是X1
或X2
。
但是我听说通常很好避免使用Ocaml中的类并使用模块,所以我试图像这样代表我的问题(过于简化但仍然说得好):
两个模块X1
和X2
,我希望我的函数能够在返回X1.t
或X2.t
之间动态决定。
module type X = sig
type choice
type t
(* some methods we don't care about in this instance, like
val modifySomething : t -> t *)
end
module Xbase = struct
type choice = Smth | SmthElse
end
module X1 = (
struct
include Xbase
type t = { foo : int; bar : int }
end : X)
module X2 = (
struct
include Xbase
type t = { foo : int; star : int }
end : X)
module XStatic =
struct
(* construct either an X1.t or X2.t from the string *)
let read : string -> 'a =
function
| "X1" -> { X1.foo = 0, bar = 0 }
| "X2" -> { X2.foo = 1, star = 1 }
end
但是Error: Unbound record field label X1.foo
函数中的read
失败了。
我已尝试过不同的方式来安排它,例如使用let open X1 in { foo = 0, ... }
但无济于事。
我的方法是否从根本上是错误的(即我应该使用类,因为这对模块来说是不可能的/不实用的)或者我只是缺少一些微不足道的东西?
修改:澄清了我尝试解决的问题,并将module X
重命名为module XBase
,以区别于module type X
。
答案 0 :(得分:6)
最简单的方法是使用sum类型(免责声明:我没有尝试编译代码):
module X1 = struct
type t = { foo : int; bar : string }
let modify_foo = ...
end
module X2 = struct
type t = { foo : int; star : bool }
let modify_foo = ...
end
type x1_or_x2 =
| Left of X1.t
| Right of X2.t
let read = function
| "X1" -> Left { X1.foo = 1; bar = "bar" }
| "X2" -> Right { X2.foo = 1; star = true }
let modify_foo = function
| Left x1 -> Left (X1.modify_foo x1)
| Right x2 -> Right (X2.modify_foo x2)
如果您想利用X1.t
和X2.t
共享某些常见结构的事实,您可以对类型进行分解。这个想法是它们分别与产品类型同构common_part * specific_to_x1
和common_part * specific_to_x2
。因此x1_or_x2
类型为(common * specific_to_x1) + (common * specific_to_x2)
,相当于common * (specific_to_x1 + specific_to_x2)
。
type common = { foo : int }
let modify_foo_common : common -> common = ...
type specific_x1 = { bar : string }
type specific_x2 = { star : bool }
type x1_or_x2 = common * specific_x1_or_x2
and specific_x1_or_x2 =
| Left of X1.t
| Right of X2.t
let read = function
| "X1" -> { foo = 1 }, Left { bar = "bar" }
| "X2" -> { foo = 1 }, Right { star = true }
let modify_foo (common, specific) = (modify_foo_common common, specific)
这样,作用于公共部分的定义不会重复,但可以声明一次。
PS:也看到这个非常相关的问题,你可能会感兴趣,哪个有一个很好的答案(镜头!):Ptival: Statically “extend” a record-ish data type without indirection hassle
答案 1 :(得分:1)
错误Unbound record field label X1.foo
是由于X1
和X2
具有模块签名Xbase
,其中只有choice
类型(而不是输入t
)。
即语法: X
隐藏了不属于X1
签名的Xbase
的所有值和类型。
但即使您更正了此错误,也会出现更重要的内容:函数read
的返回类型是什么?它不能同时为X1.t
和X2.t
。
答案 2 :(得分:1)
一旦您将模块X1
和X2
强制转换为模块类型X
,您就会在这些模块之外丢失有关内部类型结构的信息。其余代码(除模块内容之外的所有内容)将无法知道X1.t
和X2.t
的构成:这些类型变为 abstract 除了他们各自的模块之外的其他一切。
问题的OOP方法可能是制作默认构造函数,并在需要时调用它们。在您的情况下,它意味着在模块中定义默认值,在接口中添加声明(模块类型),以便外部代码仍然可以创建这些类型的值。
module type X = sig
type t
val default : unit -> t
(* etc. *)
end;;
module X1 : X = struct
include XBase
type t = {foo : int; bar : int}
(* here I can change fields adlib *)
let default () = {foo = 0; bar = 1}
(* ... *)
end;;
(* here I don't have access to X1.t fields anymore *)
请注意,如果您的类型不包含可变字段或引用,则可以将default
设为t
的简单值,而不是函数,但也许您希望保留此类型的可能性其他模块中的字段。
这里的要点是,你需要保持你的类型公开,或者提供构建和操作值的方法(后者已经在我的代码中已经在某种程度上实现了),用于抽象类型。
如果您没有或无法更改模块类型X
,那么您可以在较高级别强制执行签名,使用mli文件对除包含{{1}的模块之外的其他模块执行强制操作}和X1
(也就是说,如果您的代码遵循这样的设计)。
另一种方法是在进行强制时添加有关X2
的类型信息。例如,这是t
类型的声明,其中X1
类型已被明确表示(假设它已存在于choice
中):
X
您可以使用类型module X1 : X with type choice = XBase.choice =
struct
(* body of the X1 module *)
end;;
(* now I can access X1.choice constants from here, if XBase.choice is visible too *)
执行类似操作,然后从外部代码访问其内容。从设计的角度来看,如果您已经在模块中提供操作类型t
的值的操作符,那么它显然不是处理问题的最佳方法。