OO with rere? how to work-around?

I’ll try to give you the essence of OO emulation with different kinds of implementations, that will basically do the same simple and stupid thing. They all state the same things:

  • an entity is something that can be printed and updated (definition 1)
  • any thing that can be printed and updated is an entity (proposition 1)
  • any entity can be printed (proposition 2)
  • any entity can be updated (proposition 3)

I have some difficulty to imagine a simpler set of propositions. The first one is a definition of an entity (a type in OCaml parlance) and the three others are propositions that follow trivially from the definition.

Let’s now look at different ways to state that in OCaml, beginning with objects which is what you want to emulate. But before that, we will define your three previous entities, each of them in their own module.

module A = struct
  type t = int
  let print a = Printf.printf " { %d }\n" a
  let update a = a + 1
end

module B = struct
  type t = float
  let print a = Printf.printf " { %g }\n" a
  let update a = a +. 0.1
end

module C = struct
  type t = int * int
  let print (c1, c2) = Printf.printf " { %d %d }\n" c1 c2
  let update (c1, c2) = (c1 + 10, c2 + 10)
end

Reference implementation with objects

module Entity = struct
  (* here the definition 1 from above *)
  type t = < 
    print : unit -> unit;
    update : unit -> t;
  >

  (* now the proposition 1, which is how we construct an entity *)
  let make print_self update_self self : t = object
    val e = self
    method print () = print_self e
    method update () = {< e = update_self e >}
  end

  (* proposition 2 *)
  let print (e : t) = e#print ()

  (* proposition 3 *)
  let update (e : t) = e#update ()
end

And we can use them, as in the previous comments:

let () =
  let a = Entity_obj.make A.print A.update 3 in
  let b = Entity_obj.make B.print B.update 4.0 in
  let c = Entity_obj.make C.print C.update (10, 20) in
  [a; b; c;]
  |> List.map Entity_obj.update
  |> List.iter Entity_obj.print
;;
 { 4 }
 { 4.1 }
 { 20 30 }

Implementation with closures as previously

Now, we repeat the previous implementation with closures:

module Entity_closure = struct
  (* definition 1 *)
  type t = {
    print : unit -> unit;
    update : unit -> t;
  }

  (* proposition 1 *)
  let rec make print_self update_self self =
    let print () = print_self self in
    let update () = make print_self update_self (update_self self) in
    {print; update}

  (* proposition 2 *)
  let print e = e.print ()

  (* proposition 3 *)
  let update e = e.update ()
end

The definition is similar to the object case but with a record notation. Since the type is recursive we need to write a recursive constructor make, and here the instance variable self is hidden in each closures. You could note how the functional update is done with the recursive call to make and compare the way it’s done with object.

Implementation with GADT

Here, we keep the same interface as the previous ones, but the type t of entity is defined with a GADT.

module Entity_gadt = struct
  (* definition 1 *)
  type t = E : {
    print : 'a -> unit;
    update : 'a -> 'a;
    self : 'a;
  } -> t

  (* proposition 1 *)
  let make print update self = E {print; update; self;}

  (* proposition 2 *)
  let print (E e) = e.print e.self

  (* proposition 3 *)
  let update (E e) = E {e with self = e.update e.self}
end

Here, as with object, the instance variable self is hidden behind the existential GADT. We cannot access to self from the outside, but only via its methods. The definition of make is simpler than with object (you just have to pack the arguments together), and what is done with the definition of the methods in the object case is done, here, in the definition of the functions print and update (you could compare how similar they are).

Introduction of first-class module

I will let you do this by yourself (examples have already been given previously in the thread). First-class modules are used to only simplify calls to the make function. All the three previous implementations have the same interface:

module type Entity = sig
  (* definition 1 *)
  type t
  (* proposition 1 *)
  val make : ('a -> unit) -> ('a -> 'a) -> t
  (* proposition 2 *)
  val print : t -> unit
  (* proposition 3 *)
  val update : t -> t
end

and if you want to build an entity of kind A you have to pass its methods one by one:

make A.print A.update a

With first-class module you will have this common interface:

module type Entity = sig
  (* begin definition 1 *)
  module type S = sig
    type t
    val print : t -> unit
    val update : t -> t
  end

  type 'a meth = (module S with type t = 'a)
  type t
  (* end definition 1 *)

  (* proposition 1 *)
  val make : 'a meth -> 'a -> t
  (* proposition 2 *)
  val print : t -> unit
  (* proposition 3 *)
  val update : t -> t
end

and now, to build an entity of kind A, you just have to pass its methods all at once:

make (module A) a

I hope this will make the problem (and its solution) clearer to you.

3 Likes