编辑:我用模块类型A和B替换抽象示例,使用组和环更具体的例子。
我使用众所周知的代数结构在一个例子中提出了仿函数的问题。为组定义签名:
module type Group =
sig
type t
val neutral: t
val add: t -> t -> t
val opp: t -> t
end
为包含许多有用操作的组定义签名,例如在这里结合:
module type Extended_Group =
sig
include Group
val conjugate: t -> t -> t
end
共轭的实现仅取决于加法和反向,所以我不想明确地为我定义的所有组写它,所以我写下面的仿函数:
module F (G: Group) =
struct
include G
let conjugate x y = add (add x y) (opp x)
end
现在,假设您正在使用"扩展"组的概念,例如整数环:
module Z =
struct
(* Group *)
type t = int
let neutral = 0
let add x y = x+y
let opp x = -x
(* Ring *)
let m_neutral = 1
let mult x y = x*y
end
由于环是一个粒子组的情况,我们可以将仿函数F应用于它:
module Extended_Z = F(Z)
不幸的是,要将F应用于Z,Ocaml首先将Z的类型限制为Group,然后应用F. 一切都表现得像我这样做:
module (Group_Z: Group) = Z
module Extended_Z = F(Group_Z)
因此,尽管Z.mult有意义,但编译器不知道Extended_Z.mult。我不想失去Extended_Z仍然是戒指的事实!当然,解决方案包括编写一个类似于F但是使用Ring作为输入的仿函数F_Ring并不令人满意:我不想写F_Fields,F_VectorSpace等等。
这就是我想要的:
扩展模块签名,暴露更多值的方法。 可以用另一种方式来做,限制模块的签名(使用我用于Group_Z的语法),但是我找不到放大签名的方法。这就是:
module (Ring_Extended_Z: Ring) = Extended_Z
或者,一种定义多态仿函数的方法,即定义一个接受模块的仿函数F. 签名"至少Group"并输出签名模块"至少Extended_Group"。当然, 这只有在包含时才有意义,这就是为什么我坚信OCaml中不存在这样的功能。
在互联网上搜索了几天的答案后,我认为实现这一目标的正确方法实际上是以下方法: (灵感来自Real World OCaml第9章)
module F (G: Group) =
struct
let conjugate x y = ...
end
(与以前相同但没有包含) 然后在同一点执行所有包含:
module Extended_Z =
struct
include Z
include F(Z)
end
有人可以确认这是实现我想要的好方法,而且我的第一种方法不合适 OCaml的精神?更具体地说,有人可以确认选项1和2.在OCaml中确实是不可能的吗?
编辑:关于动态打字
在我看来,这仍然是静态输入,因为如果我写
module Extended_Z = F(Z: Ring)
OCaml仍然可以静态地键入这个具有签名Ring的模块,其额外值为#34;共轭"。它需要模块级别的多态性, 我所理解的并不存在,但我认为它可以在不使类型系统动态化的情况下存在。
答案 0 :(得分:2)
我在这个答案中分享了我发现实现我想要的两个部分令人满意的解决方案。
第一个是在我的问题的最后建议的那个,并且在代数结构的情况下可能是"好的"之一。
module type Group =
sig
type t
val neutral: t
val add: t -> t -> t
val opp: t -> t
end
module type Extended_Group =
sig
include Group
val conjugate: t -> t -> t
end
module F (G: Group) =
struct
let conjugate x y = G.add (G.add x y) (G.opp x)
end
module type Ring =
sig
include Group
val m_neutral: t
val mult: t -> t -> t
end
module Z =
struct
type t = int
let neutral = 0
let add x y = x+y
let opp x = -x
(* Ring *)
let m_neutral = 1
let mult x y = x*y
end
module Extended_Z =
struct
include Z
include F(Z)
end
在这种情况下,由OCaml推断的Extended_Z的签名"扩展"签名Ring,实际上Extended_Z.mult在模块外可见。
第二种解决方案使用了Andreas Rossberg和Ivg建议的模块级别的多态性模拟。在代数结构的情况下,它不太令人满意,但对于我的实际问题,它是完美的。
module type Group_or_More =
sig
include Group
module type T
module Aux: T
end
module F (G: Group_or_More) =
struct
include G
let conjugate x y = add (add x y) (opp x)
end
module Z =
struct
type t = int
let neutral = 0
let add x y = x + y
let opp x = -x
module type T =
sig
val m_neutral: t
val mult: t -> t -> t
end
module Aux =
struct
let m_neutral = 1
let mult x y = x*y
end
end
module Extended_Z = F(Z) ;;
在这里,签名Group_or_More正好捕获了我的签名扩展另一个的想法。你把所有额外的东西放到辅助模块Aux中。但是,为了能够提供所需数量的辅助功能,您可以将此Aux模块的签名指定为组的一部分。
对于代数结构,它不太令人满意,因为Z和Extended_Z是一个较弱意义上的环:乘法由Z.Aux.mult和Extended_Z.Aux.mult访问。
答案 1 :(得分:0)
Functors是结构上的抽象,从结构中产生结构。事实上,它们是绝对的态射,即箭头。这是一个非常强大的机制,但仍有一个限制:模块的所有参数必须具有固定(常量)模块类型,即没有模块类型变量。这意味着,不可能得到你想要的东西,有时你需要编写比你想象的更多的代码。
您的示例实际上捕获了最小(最小)结构和最大(最大)的概念。前者提炼出代数结构的最小要求,而后者描述了可以从中导出的最大元素集(其中元素表示为结构域)。将它与Haskell的类型类进行比较,您可以在其中提供最小的实现,其余的由类型类定义派生。同样,可以用仿函数表达,但与Haskell不同,没有机器可以自动应用这些仿函数,您需要手动实例化所有仿函数。这与OCaml意识形态一致 - 显性优于隐式。 (尽管通过提供与Haskell类型类相似的推理机制,即将出现的模块化含义将与这种意识形态背道而驰)。
但是有足够的话,让我们做一些编码。正如我所说,你的例子,为最小和最大结构提供了一个很好的例子。我们将首先定义相应代数结构的最低要求,试图密切关注定义:
module Min = struct
module type Set = sig
type t
val compare : t -> t -> int
end
module type Monoid = sig
include Set
val zero : t
val (+) : t -> t -> t
end
module type Group = sig
include Monoid
val (~-) : t -> t
end
module type Ring = sig
include Group
val one : t
val ( * ) : t -> t -> t
val (~/) : t -> t
end
...
end
我们将所有模块类型定义放入模块Min
,表示它们是最小的。这些只是模块类型,即签名。
现在,我们可以定义最大签名。这里的问题是通常很难停止,即找到最大的固定点。但这不是一个大问题,因为你可能会继续编写带有更小结构并返回更大的仿函数,直到你对结果满意为止。
例如,我们可以通过输入类型t和compare
来推断很多事情:
module Max = struct
module type Set = sig
include Min.Set
val (=) : t -> t -> t
val (<) : t -> t -> t
val (>) : t -> t -> t
module Set : Set.S (* this is Set from stdlib, not ours *)
module Map : Map.S
(* and much more *)
end
module Ring = sig
include Min.Ring
...
end
...
end
现在,当我们有一个很好的结构理论时,我们可以转向实现。我们可以为所有签名Min.T -> Max.T
提供通用态射T
,例如,
module Set(S : Min.Set) : Max.Set with type t = S.t = struct
include S
let (<) x y = compare x y = -1
...
module Set = Set.Make(S)
module Map = Map.Make(S)
...
end
使用这些通用仿函数作为基本工具,以及我们的最小定义,我们可以构建一个真实类型的结构,我们正在定义。此外,由于通用仿函数通常无法为某些操作提供最佳实现,因此我们可以提供自己的仿函数,以充分利用我们对实现的了解。例如:
module Z = struct
module T = struct
type t = float
let compare = compare
let one = 1.
let zero = 0.
let (+) = (+.)
let (~-) = (~-.)
let (~/) x = (1. /. x)
end
include Set(T)
include Ring(T)
(* override minus and division as we can implement them more efficiently *)
let (-) = (~.)
let (/) = (/.)
end
我们可以非常轻松地描述我们的具体Z
模块的模块类型,只需包含相应的最大签名并添加我们的特定内容:
module type Z = sig
include Ring
val read : in_channel -> t
val write : out_channel -> t -> unit
end
在Andreas Rossberg的评论中指出,OCaml模块子语言实际上至少在语法上允许多态性。这是一个系统F样式多态,必须由L
术语明确引入,术语是绑定类型而不是值的术语。由于模块可以携带类型,因此我们可以使用具有以下模块类型的模块来表达L
术语:
module type L = sig module type T end
现在,我们可以定义经典的多态函数id
,fst
和snd
:
module Id(L:L)(X:L.T) = X
module Fst(L1:L)(L2:L)(M1 : L1.T)(M2 : L2.T) = M1
module Snd(L1:L)(L2:L)(M1 : L1.T)(M2 : L2.T) = M2
然而,看起来我们对函数定义右侧的参数的处理非常有限。例如,我们无法组合两个模块:
module Sum(L1:L)(L2:L)(M1 : L1.T)(M2 : L2.T) = struct
include M1
^^^^^^^^^^
Error: This module is not a structure; it has type L1.T
include M2
end
我们也无法打开模块,因为我们遇到了同样的错误。问题是include
和open
语句正在评估模块类型变量的值,期望获得签名。由于打字环境尚未对L1.T
进行绑定,因此我们得到了一种误导性的错误消息。鉴于此限制,我们只能使用标识符级别的参数。例如,我们仍然可以将Sum
模块定义为:
module Sum(L1:L)(L2:L)(M1 : L1.T)(M2 : L2.T) = struct
module X = M1
module Y = M2
end
但是,它是否有任何实际用途是非常值得怀疑的。另一个例子是定义App
仿函数,它将一个仿函数应用于参数(也是可疑的可用性):
module App(L1:L)(L2:L)
(F : functor (X : L1.T) -> L2.T)(X:L1.T) = struct
module R = F(X)
end
问题的根源在于OCaml试图在仿函数应用程序发生之前,即在定义时访问绑定到参数的值。这可能被视为编译器错误,但由于语言中没有真正的L
- 术语,我们只是用一个可以承载类型的参数来模仿它。显然这还不够,因为算子的评估策略与在系统F中正确消除L
项无关。但即使如果我们在这里有一个真正的系统F,我们仍然不会能够应用include
或open
抽象值,因为这些操作仅为结构定义,而不是为仿函数定义。因此,我们仍然可以应用对仿函数和模块都有效的操作。