如何从函数 return first-class 模块的嵌套类型的实例?

How to return the instance of first-class module's nested type from a function?

上下文:

我正在尝试使用 first-class 模块在 OCaml 中实现类似 OOP 可观察模式的东西。我有一个包含模块列表的项目,并希望通过观察而不更改地扩展它们。为了尽量减少代码重复,我创建了 Subject 模块,并计划将其用作此扩展的通用方式(在项目上下文中)的一部分。我声明了三种模块类型:

观察者:

module type OBSERVER = sig
  type event
  type t

  val send : event -> t -> t
end

观察:

module type OBSERVABLE = sig
  type event
  type subscr
  type t

  module type OBSERVER = OBSERVER with type event = event

  val subscribe   : (module OBSERVER with type t = 't) -> 't -> t -> (subscr * t)
  val unsubscribe : subscr -> t -> t
end

and SUBJECT 正在合并 OBSERVEROBSERVABLE:

module type SUBJECT = sig
  include OBSERVER
  include OBSERVABLE 
     with type event := event
      and type t := t
end

接下来我要实现的是 Subject 模块。 该模块的职责是将许多OBSERVER聚合为一个。 当然,他们应该处理相同的 event 类型,这就是我实施“Subject”(Subject.Make) 作为函子。

module Subject = struct
  module Make (Event : sig type t end) : sig
    include SUBJECT with type event = Event.t 
    val empty : t
  end = struct
    type event = Event.t
    module type OBSERVER = OBSERVER with type event = event
...

要存储 OBSERVER 的第一个 class 模块的实例,并且能够添加和删除(以任何顺序)它们,我使用 Map with int as key (即 subscr).

...
    type subscr = int 
    module SMap = Map.Make (Int)
...

正如我们从 OBSERVER (val send : event -> t -> t) 中的 send 签名中看到的那样,不仅需要存储实例OBSERVER 的第一个 class 模块,还有 states 个(“[=315= 的实例]").由于类型不同,我无法将所有 states 存储在一个集合中。所以我声明了模块类型 PACK 来将 OBSERVER 的第一个-class 模块的实例和它的状态实例打包到实例中.

...
    module type PACK = sig
      module Observer : OBSERVER
      val state : Observer.t    
    end

    type t =
      { next_subscr : subscr;
          observers : (module PACK) SMap.t
      }

    let empty =
      { next_subscr = 0;
        observers = SMap.empty
      }

    let subscribe (type t)
        (module Obs : OBSERVER with type t = t) init o =
      o.next_subscr,
      { next_subscr = succ o.next_subscr;
        observers = o.observers |> SMap.add 
                      o.next_subscr
                      ( module struct
                          module Observer = Obs
                          let state = init
                        end : PACK
                      ) 
      }

    let unsubscribe subscription o =
      { o with
        observers = o.observers |> SMap.remove subscription 
      }
...

Subjectsend 函数在新的 state 中重新打包每个 pack 并在旧的 Observer 模块中。

...
    let send event o =
      let send (module Pack : PACK) = 
        ( module struct
            module Observer = Pack.Observer
            let state = Observer.send event Pack.state
          end : PACK
        ) in
      { o with
        observers = SMap.map send o.observers
      }
  end
end

为了测试 Subject 并查看在没有更改的情况下进行观察扩展的模块的外观 - 我创建了一些模块 Acc

module Acc : sig 
  type t
  val zero : t
  val add : int -> t -> t
  val multiply : int -> t -> t
  val value : t -> int
end = struct
  type t = int
  let zero = 0
  let add x o = o + x
  let multiply x o = o * x
  let value o = o
end

并在模块 OAcc 中使用观察功能扩展它,并使用以下签名合并 OBSERVABLE 和原始模块类型 加速度

module OAcc : sig 
  type event = Add of int | Multiply of int

  include module type of Acc
  include OBSERVABLE with type event := event
                      and type t := t 
end = 
...

我实施了OAcc,将观察责任委托给主题,主要责任委托给原始Acc.

...
struct
  type event = Add of int | Multiply of int      
  module Subject = Subject.Make (struct type t = event end)
  module type OBSERVER = Subject.OBSERVER                         
  type subscr = Subject.subscr
  type t = 
    { subject : Subject.t;
      acc : Acc.t
    }

  let zero = 
    { subject = Subject.empty;
      acc = Acc.zero
    } 
  let add x o = 
    { subject = Subject.send (Add x) o.subject;
      acc = Acc.add x o.acc
    } 
  let multiply x o = 
    { subject = Subject.send (Multiply x) o.subject;
      acc = Acc.multiply x o.acc
    }

  let value o = Acc.value o.acc

  let subscribe (type t) (module Obs : Subject.OBSERVER with type t = t) init o =
    let subscription, subject = 
      Subject.subscribe (module Obs) init o.subject in
    subscription, { o with subject }

  let unsubscribe subscription o =
    { o with subject = Subject.unsubscribe subscription o.subject
    } 
end 

创建了一些仅将操作打印到控制台的“OBSERVER 模块”

module Printer : sig 
  include OAcc.OBSERVER
  val make : string -> t
end = struct
  type event = OAcc.event
  type t = string
  let make prefix = prefix
  let send event o = 
    let () = 
      [ o;
        ( match event with
          | OAcc.Add      x -> "Add("      ^ (string_of_int x) 
          | OAcc.Multiply x -> "Multiply(" ^ (string_of_int x)
        );
        ");\n"
      ] 
      |> String.concat ""
      |> print_string in
    o
end

最后,我创建了函数 print_operations 并测试了所有功能是否按预期工作

let print_operations () =
  let p = (module Printer : OAcc.OBSERVER with type t = Printer.t) in 
  let acc = OAcc.zero in
  let s1, acc = acc |> OAcc.subscribe p (Printer.make "1.") in 
  let s2, acc = acc |> OAcc.subscribe p (Printer.make "2.") in 
  let s3, acc = acc |> OAcc.subscribe p (Printer.make "3.") in
  acc |> OAcc.add 1
      |> OAcc.multiply 2
      |> OAcc.unsubscribe s2 
      |> OAcc.multiply 3
      |> OAcc.add 4 
      |> OAcc.unsubscribe s3
      |> OAcc.add 5
      |> OAcc.unsubscribe s1
      |> OAcc.multiply 6
      |> OAcc.value

调用后print_operations ();;我有以下输出

# print_operations ();;

1.Add(1);
2.Add(1);
3.Add(1);
1.Multiply(2);
2.Multiply(2);
3.Multiply(2);
1.Multiply(3);
3.Multiply(3);
1.Add(4);
3.Add(4);
1.Add(5);

- : 整数 = 90

当我们的第一个class模块observer的逻辑完全基于副作用并且我们不需要它的状态时,一切正常主题 之外。但是对于相反的情况,我没有找到任何关于如何从 Subject[ 中提取订阅 observerstate =233=].

例如,我有以下“OBSERVER” (在这种情况下,访问者多于观察者)

module History : sig 
  include OAcc.OBSERVER
  val empty : t
  val to_list : t -> event list
end = struct
  type event = OAcc.event
  type t = event list
  let empty = []
  let send event o = event :: o
  let to_list = List.rev
end

我可以订阅 History 的第一个 class 实例和它的一些初始状态 OAcc 但我不不知道怎么提取回来。

let history_of_operations () = 
  let h = (module History : OAcc.OBSERVER with type t = History.t) in 
  let acc = OAcc.zero in
  let s, acc = acc |> OAcc.subscribe h History.empty in
  let history : History.t = 
    acc |> OAcc.add 1
        |> OAcc.multiply 2 
        |> failwith "implement extraction of History.t from OAcc.t" in
  history


我想做什么。我在OBSERVABLE中更改了unsubscribe的签名。在它之前 return 是“OBSERVABLE”的状态,没有“OBSERVER”与提供的订阅关联,现在它是 return 此状态的三元组,首先取消订阅-class 模块,以及取消订阅模块的状态。

之前:

module type OBSERVABLE = sig
  ...
  val unsubscribe : subscr -> t -> t
end

之后:

module type OBSERVABLE = sig
  ...
  val unsubscribe : subscr -> t -> (t * (module OBSERVER with type t = 't) * 't))
end

OBSERVABLE 是可编译的,但我无法实现它。 以下示例显示了我的尝试之一。

module Subject = struct
  module Make (Event : sig type t end) : sig
...
  end = struct
...
    let unsubscribe subscription o =
      let (module Pack : PACK) =
        o.observers |> SMap.find subscription
      and observers = 
        o.observers |> SMap.remove subscription in 
      { o with observers },
      (module Pack.Observer : OBSERVER),
      Pack.state
...
  end
end

因此,我有:

    Pack.state 
    ^^^^^^^^^^

Error: This expression has type Pack.Observer.t
but an expression was expected of type 'a
The type constructor Pack.Observer.t would escape its scope

问题 1:

是否可以用这个签名实现取消订阅


没用。我尝试了另一种解决方案。 它基于 取消订阅 可以 return PACK 的第一个 class 模块的实例的想法。 我更喜欢前面的想法,因为它使 PACK 的声明在 Subject 中保持私有。但是当前的解决方案在寻找解决方案方面取得了更好的进步。

我将 PACK 模块类型添加到 OBSERVABLE 并将 unsubscribe 签名更改为以下内容。

module type OBSERVABLE = sig
...
  module type PACK = sig
    module Observer : OBSERVER
    val state : Observer.t    
  end
...
  val unsubscribe : subscr -> t -> (t * (module PACK))
end

PACK 添加到 OAcc 实现中,因为它的签名包括 OBSERVABLE。另外,我重新实现了 unsubscribe of OAcc.

module OAcc : sig 
...
end = struct
...
  module type PACK = Subject.PACK
...       
  let unsubscribe subscription o =
    let subject, ((module Pack : PACK) as p) = 
      Subject.unsubscribe subscription o.subject in
    { o with subject }, p 
end 

Subject 的实现已经包含 PACK,因此无需添加。 仅 unsubscribe 被重新实现。

module Subject = struct
  module Make (Event : sig type t end) : sig
...
  end = struct
...
    let unsubscribe subscription o = 
      let ((module Pack : PACK) as p) =
        o.observers |> SMap.find subscription
      and observers = 
        o.observers |> SMap.remove subscription in 
      { o with observers }, p
...
  end
end 

最后,我创建了我更改了 history_of_operations 以测试解决方案

let history_of_operations () = 
  let h = (module History : OAcc.OBSERVER with type t = History.t) in 
  let acc = OAcc.zero in
  let s, acc = acc |> OAcc.subscribe h History.empty in
  let acc, (module Pack : OAcc.PACK) = 
    acc
    |> OAcc.add 1
    |> OAcc.multiply 2 
    |> OAcc.unsubscribe s in
  Pack.state ;;

调用后history_of_operations ();;出现错误

  Pack.state
  ^^^^^^^^^^

Error: This expression has type Pack.Observer.t
but an expression was expected of type 'a
The type constructor Pack.Observer.t would escape its scope

另外,我试过了

let history_of_operations () = 
...
    History.to_list Pack.state

但是

  History.to_list Pack.state
                  ^^^^^^^^^^

Error: This expression has type Pack.Observer.t
but an expression was expected of type History.t

问题 2:

如何从类型为 List.t?

Pack 中提取状态

我更改了退订

的签名
module type OBSERVABLE = sig
...
  val unsubscribe : subscr -> t -> (t * (module PACK with type Observer.t = 't))
end

并尝试在 Subject

中重新实现 unsubscribe
module Subject = struct
  module Make (Event : sig type t end) : sig
...
  end = struct
...
    let unsubscribe (type t) subscription o = 
      let ((module Pack : PACK with type Observer.t = t) as p) =
        o.observers |> SMap.find subscription
      and observers = 
        o.observers |> SMap.remove subscription in 
      { o with observers }, p
...
  end
end 

但是

      o.observers |> SMap.find subscription
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Error: This expression has type (module PACK)
but an expression was expected of type
(module PACK with type Observer.t = t)

看起来 OCaml 有 3 个类型抽象级别
1.具体module A : sig type t = int end = struct ...
2.摘要module A : sig type t end = struct ...
3.打包到first-class模块

问题 3:

是否可以使用(2)抽象级别[=252=存储第一个class模块的嵌套实例类型] 或者能够将其恢复到 (2) 抽象级别 ?


题目中的问题:

如何从函数return第一个class模块的嵌套类型的实例?


备注:

当然,可以通过使用可变状态来解决这个问题,但问题不在于此。

初始可编译源代码here

关于您的问题 1,您期望一个带有签名的函数:

val unsubscribe : subscr -> t -> (t * (module OBSERVER with type t = 't) * 't))

模块的存在在这里是一个转移注意力的问题。你的签名和

没有区别
val unsubscribe : subscr -> t -> 'a

换句话说,它是一个神奇地 returns 调用者可能需要的任何类型的值的函数。如果调用者想要一个整数,函数 returns 一个整数。如果调用者想要一个字符串,函数 returns 一个字符串。等等。因此,只有一种具有这种签名的安全函数,它是一种永远不会 returns 任何东西的函数。

因此,您需要将量化转移到其他地方的类型上,例如在构造函数下:

type 'u unsubscribe_result = UResult: 'u *  (module OBSERVER with type t = 't) * 't -> 'u unsubscribe_result
val unsubscribe : subscr -> t -> t unsubscribe_result

简短的回答是,打包模块的内部类型永远不能被提升到它们的第一个 class 模块之外。

当您将 packed observer 定义为:

  module type PACK = sig
    module Observer: sig
      type t
      val send: event -> t -> t
    end
    val state: Observer.t
  end 

类型 Observer.t 在第一个 class 模块中存在量化:通过将初始实现打包在 (module PACK) 中,我忘记了我对初始模块的所有了解,除了模块内部的类型相等。 这意味着对于 (module PACK) 类型的值 (module M),我唯一可用的操作是调用 M.Observer.send event M.state。 也就是说,(module PACK)其实等价于下面的类型

type send = { send: event -> send }

Observer 的状态更明显无法访问。

因此,当您将观察员装在

中时,您的问题就开始了
    let subscribe (type t)
        (module Obs : OBSERVER with type t = t) init o =
      o.next_subscr,
      { next_subscr = succ o.next_subscr;
        observers = o.observers |> SMap.add 
                      o.next_subscr
                      ( module struct
                          module Observer = Obs
                          let state = init
                        end : PACK
                      ) 
      }

在这里,当您打包模块 Obs 时,您实际上忘记了 Obs 的类型并放弃了对该类型的任何进一步使用。

如果你想取回观察者的状态,你必须保留类型信息。一个好的起点是查看 OBSERVABLE 签名:

module type OBSERVABLE = sig
  type event
  type subscr
  type t

  module type OBSERVER = OBSERVER with type event = event
  val subscribe : (module OBSERVER  with type t = 't) -> 't -> t -> (subscr * t)
  val unsubscribe : subscr -> t -> t
end

并注意到我们开始丢失 subscribe 中的类型信息,因为我无法将特定的 subscr 与可观察类型相关联。因此,一种解决方案是通过使用订阅观察者的类型参数化 subscr 来保留此信息:

module type OBSERVABLE = sig
  type event
  type 'a subscr
  type t

  module type OBSERVER = OBSERVER with type event = event
  val subscribe : (module OBSERVER  with type t = 't) -> 't -> t -> ('t subscr * t)
  val unsubscribe : 't subscr -> t -> t
end

然后,通过这个改变,unsubscribe可以return观察者的当前状态,因为我们知道这个状态的类型:它是订阅存储的类型:

  val unsubscribe : 't subscr -> t -> t * 't

因此,剩下的问题是将观察者存储在地图中,地图的类型取决于插入它们的键的类型。此约束指向异构映射。使用 hmap 库,可以通过以下方式完成:


module Subject = struct
  module Make (Event : sig type t end) : sig
    include SUBJECT with type event = Event.t
    val empty : t
  end = struct
    type event = Event.t
    module type OBSERVER =
      OBSERVER with type event = event
    (* we need to keep the module implementation with the key for map *)
    module HM = Hmap.Make(struct type 'a t = (module OBSERVER  with type t = 'a) end)
    type t = HM.t
    type 'a subscr = 'a HM.key


    let empty = HM.empty

    let subscribe (type t)
        (((module Obs) :  (module OBSERVER  with type t = t) ) as vt) (init:t) o =
      let key: t subscr = HM.Key.create vt in
      key, HM.add key init o

    let unsubscribe subscription o =
      HM.rem subscription o, HM.get subscription o

    let send event o =
      let send_and_readd (HM.B(k,s)) o =
        let module Obs = (val HM.Key.info k) in
        let s = Obs.send event s in
        HM.add k s o in
      HM.fold send_and_readd o empty
  end
end

免责声明:我不会假装我完全理解你的问题,这是迄今为止我在 SO 上看到的最大的 OCaml 相关问题。但我的直觉告诉我,你正在寻找存在主义。

没有类型相等性的简单存在

在这种方法中,我们可以将对象接口及其状态打包到一个存在的 GADT 中。我们将能够使用状态,只要它不脱离其定义的范围,这将是解压缩我们存在的功能。有时,这正是我们想要的,但我们将在下一节中扩展这种方法。

让我们从一些初步的定义开始,让我们定义我们想要打包的对象的接口,例如,像这样:

module type T = sig
  type t
  val int : int -> t
  val add : t -> t -> t
  val sub : t -> t -> t
  val out : t -> unit
end

现在,我们可以将此接口与状态(t 类型的值)一起打包到存在

type obj = Object : {
    intf : (module T with type t = 'a);
    self : 'a
  } -> obj

然后我们可以轻松地解包界面和状态,并将界面中的任何功能应用到状态。因此,我们的类型 t 是纯粹抽象的,而且确实是 existential types are abstract types,例如

module Int = struct
  type t = int
  let int x = x
  let add = (+)
  let sub = (-)
  let out = print_int
end

let zero = Object {
    intf = (module Int);
    self = 0;
  }

let incr (Object {intf=(module T); self}) = Object {
    intf = (module T);
    self = T.add self (T.int 1)
  }

let out (Object {intf=(module T); self}) = T.out self

可恢复的存在(又名动态类型)

但是,如果想要恢复抽象类型的原始类型,以便我们可以应用适用于该类型值的其他函数,该怎么办。为此,我们需要存储类型 x 属于所需类型 y 的证据,我们可以使用可扩展的 GADT

来做到这一点
 type 'a witness = ..

为了创建新的见证人,我们将首先使用 class 个模块,

let newtype (type u) () =
  let module Witness = struct
    type t = u
    type _ witness += Id : t witness
  end in
  (module Witness : Witness with type t = u)

其中模块类型 Witness 及其打包类型是,

module type Witness = sig 
     type t 
     type _ witness += Id : t witness
end

type 'a typeid = (module Witness with type t = 'a)

每次调用 newtype 时,它都会向见证类型添加一个新的构造函数,保证不等于任何其他构造函数。为了证明两个 witness 实际上是用相同的构造函数创建的,我们将使用以下函数,

let try_cast : type a b. a typeid -> b typeid -> (a,b) eq option =
  fun x y ->
  let module X : Witness with type t = a = (val x) in
  let module Y : Witness with type t = b = (val y) in
  match X.Id with
  | Y.Id -> Some Equal
  | _ -> None

其中returns定义为的等式证明,

type ('a,'b) eq = Equal : ('a,'a) eq

在我们可以构建类型 (x,y) eq 的对象的环境中,类型检查器将处理类型 xy 具有相同类型的值。有时,当您确实确定转换必须成功时,您可以使用 cast 函数,

let cast x y = match try_cast x y with
  | None -> failwith "Type error"
  | Some Equal -> Equal

作为,

let Equal = cast t1 t2 in
(* here we have proved that types witnessed by t1 and t2 are the same *)

好的,现在当我们有了动态类型时,我们可以使用它们来使我们的对象类型可恢复和状态可转义。我们需要的只是将运行时信息添加到我们的对象表示中,

type obj = Object : {
    intf : (module T with type t = 'a);
    self : 'a;
    rtti : 'a typeid;
  } -> obj

现在让我们定义类型 int 的运行时表示(请注意,通常我们可以在 rtti 中放置更多信息,除了 witness,我们还可以将其设为排序类型并在运行时扩展动态类型使用新的操作,并实现临时多态性),

let int : int typeid = newtype ()

所以现在我们的 zero 对象定义为,

let zero = Object {
    intf = (module Int);
    self = 0;
    rtti = int;
  }

incr 函数仍然相同(模对象表示中的一个额外字段),因为它不需要转义。但是现在我们可以编写 cast_object 函数,它将采用所需的类型并将对象转换为它,

let cast_object (type a) (t : a typeid) (Object {self; rtti}) : a option =
  match try_cast t rtti with
  | Some Equal -> Some self
  | None -> None

# cast_object int zero;;
- : int option = Some 0
# cast_object int (incr zero);;
- : int option = Some 1

另一个例子,

let print_if_int (Object {self; rtti}) =
  match try_cast int rtti with
  | Some Equal -> print_int self
  | None -> ()

您可以阅读有关动态类型的更多信息here。 OCaml 中也有很多库提供动态类型和异构字典等。