A few years back, Stephen wrote a fun post about how to build a so-called "universal type" in OCaml. Such a type allows you to embed any other type within in it, letting you do things like creating ad-hoc lists containing elements of multiple different types.

I've been thinking about universal types again because I've been working on a project lately that uses a universal type as a central architectural piece. Most ML programmers find the idea of a universal type a little jarring, and so I've been thinking about how to present it to make it easy to understand. Perhaps the first thing to consider is what the signature should look like. Here's what I started with:

  1. module Univ : sig
  2. module Type : sig
  3. type 'a t
  4. val create : unit -> 'a t
  5. end
  6.  
  7. type t
  8. val embed : 'a Type.t -> 'a -> t
  9. val project : 'a TYpe.t -> t ->'a option
  10. end
  11.  

And here's a simple example of Univ in action.

  1. let int_type = (Univ.Type.create () : int Univ.Type.t)
  2. let string_type = (Univ.Type.create () : string Univ.Type.t)
  3.  
  4. let mixed_list = [ Univ.embed int_type 3
  5. ; Univ.embed string_type "whatever"
  6. ; Univ.embed int_type 5 ]
  7.  
  8. let () =
  9. assert (List.filter_map ~f:(Univ.project int_type ) mixed_list
  10. = [3;4]);
  11. assert (List.filter_map ~f:(Univ.project string_type) mixed_list
  12. = ["whatever"]);

But there's something pointlessly confusing about this type. For one thing, the use of the term "type" makes promises that can't quite be satisfied. For instance, there's no guarantee that two values of the same type embedded into Univ.t can be reached in the same way. Consider this example.

  1. let int_type = (Univ.Type.create () : int Univ.Type.t)
  2. let int_type' = (Univ.Type.create () : int Univ.Type.t)
  3.  
  4. let mixed_list = [ Univ.embed int_type 3
  5. ; Univ.embed int_type' 4
  6. ; Univ.embed int_type 5 ]
  7.  
  8. let () =
  9. assert (List.filter_map ~f:(Univ.project int_type) mixed_list
  10. = [3;4;5]);

WHen I tried to explain what Univ was to people verbally, I described it as a kind of extensible sum type. When Stephen heard me giving this description, he proposed that we change the type signature to reflect this. We ended up with a signature that looks like this:

  1. module Univ : sig
  2. module Variant : sig
  3. type 'a t
  4. val create : unit -> 'a t
  5. end
  6.  
  7. type t
  8. val create : 'a Variant.t -> 'a -> t
  9. val match_ : 'a Variant.t -> t -> 'a option
  10. end

The names now point you in the right direction: every time you call Variant.create, you're creating a new arm of this extensible sum type. There's no guarantee that two variants won't be created with the same type, and there's no need for such a guarantee.

We also changed the embed and project to create and match_, to better track the terminology used in the rest of the language for constructing and deconstructing sum types.

Along the way, we made one other change to Univ, which was to add some default functionality to each variant. In particular, the ability to figure out the name of any variant within the Univ type, and the ability to serialize a Univ.t to an s-expression. This is useful for dynamically browsing a collection of Univ.t values at run-time. The final interface looks like this:

  1. module Univ : sig
  2. module Variant : sig
  3. type 'a t
  4. (** [create variant_name to_sexp] creates a new variant with the
  5.   given name and serializer *)
  6. val create : string -> ('a -> Sexp.t) -> 'a t
  7. end
  8.  
  9. type t
  10. val create : 'a Variant.t -> 'a -> t
  11. val match_ : 'a Variant.t -> t -> 'a option
  12.  
  13. val to_sexp : t -> Sexp.t
  14. val variant_name : t -> string
  15. end

The other thing that struck me about this is that when I first heard about it, the idea of Univ seemed interesting but mostly a curiousity --- I just didn't have any applications for it in mind.

But just because an idea isn't useful right now, doesn't mean it's not going to become useful later.