Universal type
A universal type is a type into which all other types can be embedded. A module implementing such a type here will satisfy the following signature.
module type UNIVERSAL = sig type t val embed : unit → (α → t) * (t → α option) end;;The
type t
is the universal type and each call
to embed
returns a pair of functions : an injection
function for embedding a value into the universal type and, a
projection function for extracting the value from its embedding
in the universal type. The following code demonstrates intended
usage.
module type TEST = sig val run : unit → unit end;; module type UNIVERSAL_TEST = functor (U : UNIVERSAL) → TEST;; module Basic_usage : UNIVERSAL_TEST = functor (U : UNIVERSAL) → struct let run () = let ((of_int : int → U.t) , (to_int : U.t → int option)) = U.embed () in let ((of_string : string → U.t) , (to_string : U.t → string option)) = U.embed () in let r : U.t ref = ref (of_int 13) in begin assert (to_int !r = Some 13); assert (to_string !r = None); r := of_string "foo"; assert (to_string !r = Some "foo"); assert (to_int !r = None); end end;;
One possible implementation is via the use of exceptions
together with local modules. The core idea exploits the fact that
the primitive type exn
is an open extensible
sum. Here's the complete implementation. We'll break it down
later.
module Universal_exn : UNIVERSAL = struct type t = exn module type ANY = sig type c exception E of c end type α any = (module ANY with type c = α) let mk : unit → α any = fun (type s) () → (module struct type c = s exception E of c end : ANY with type c = s) let inj (type s) (p : s any) (x : s) : t = let module Any = (val p : ANY with type c = s) in Any.E x let proj (type s) (p : s any) (y : t) : s option = let module Any = (val p : ANY with type c = s) in match y with | Any.E x → Some x | _ as e → None let embed () = let p = mk () in inj p, proj p end;;Before delving into an explanation of the program, one can verify it satisfies the basic test.
# module Test_basic = Mk_universal_test(Universal_exn);; # Test_basic.run ();; - : unit = ()
The definition of the universal type t
is an alias
to the predefined type exn
.
type t = exnA module type
ANY
is introduced. Modules that
implement this signature define an abstract type c
and
introduce an exn
constructor E of c
.
module type ANY = sig type c exception E of c endAn alias for the type of a module value satisfying this signature comes next. Using aliases of this kind are helpful in reducing "syntactic verbosity" in code accepting and returning module values.
type α any = (module ANY with type c = α)Next follow a set of functions that are private to the implementation of the module. The first of these is
mk
.
let mk : unit → α any = fun (type s) () → (module struct type c = s exception E of c end : ANY with type c = s)This function
mk
takes the unit
argument
and each invocation computes a new module instance which is packed
as a first class value and returned. The locally abstract
type s
connects to the α
in the
return type.
The next function to be defined is inj
which computes
a universal type value from its argument.
let inj (type s) (p : s any) (x : s) : t = let module Any = (val p : ANY with type c = s) in Any.E xAs was the case for
mk
, a locally abstract type is
used in the definition of inj
and observe how that
type ensures a coherence between the module
parameter p
and the type of the parameter to be
embedded x
.
The projection function proj
comes next and also uses
a locally abstract type ensuring coherence among its parameters.
let proj (type s) (p : s any) (y : t) : s option = let module Any = (val p : ANY with type c = s) in match y with | Any.E x → Some x | _ as e → NoneThe body of
proj
unpacks the module value parameter into
a module named Any
and then attempts to
match y
against the constructor defined
by Any
. Recall, at the end of the day, y
is of type exn
. The match contains two cases : the
first matching the constructor Any.E x
with x
having type s
, the second anything
else (that is, proj
is total).
Finally we come to the public function embed
.
let embed () = let p = mk () in inj p, proj p
embed
calls mk
to produce a new
embedding p
and returns the pair of partial
applications (inj p, proj p)
.
Our examination of the implementation is concluded. There are
however various implications of the implementation we are now in a
position to understand that are not immediately obvious from the
specification. As a first example, consider the inferred type of a
call to embed
.
# module U = Universal_exn;; # let inj, proj = U.embed ();; val inj : '_a → U.t =Property 1 :val proj : U.t → '_a option =
embed
produces weakly polymorphic
functions.
Consider the following scenario:
# let of_int, to_int = U.embed ();; # let of_string, to_string = U.embed ();;At this point all of
of_int
, to_int
, of_string
and to_string
are weakly polymorphic.
# let r : U.t ref = ref (of_int 13);;The call to
of_int
fixes it's type to int →
U.t
and that of to_int
to U.t → int
option
. The types of of_string
and to_string
remain unfixed.
# to_string !r = Some 13;; - : bool = falseThe programmer has mistakenly used a projection function from a different embedding.
# r := of_string "foo";; Error: This expression has type string but an expression was expected of type intThe erroneous usage of
to_string
incidentally fixed
the type of of_string
to int → U.t
! One
can imagine errors of this kind being difficult to diagnose.
Property 2 : Injection/projection functions of different embeddings may not be mixed.
# let ((of_int : int → U.t), (to_int : U.t → int option)) = U.embed ();; # let ((of_int' : int → U.t), (to_int' : U.t → int option)) = U.embed ();;Two embeddings have been created, the programmer has fixed the types of the injection/projection functions a priori (presumably as a defense against the problems of the preceding example).
# let r : U.t ref = ref (of_int 13);; # to_int !r = Some 13;; - : bool = trueThe first embedding is used to inject an integer. That integer can be extracted using the projection function of that embedding.
# to_int' !r = Some 13;; - : bool = falseWe cannot extract the integer from the universal type value using the projection function from the other embedding despite all of the types involved being the same. One can imagine that also as being quite the source of bugs and confusion to the unknowing programmer.
The constraint of property 2 is beyond the ability of the type system to enforce. About the best one can do is to enhance the specification of the type with documentation.
module type UNIV = sig type t val embed : unit → (α → t) * (t → α option) (* A pair [(inj, proj)] returned by [embed] works together in that [proj u] will return [Some v] if and only if [u] was created by [inj v]. If [u] was created by a different function then [proj u] will return [None]. *) end;;