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.