From : Jeremy Yallop <yallop AT gmail.com>

: Jeremy Yallop <yallop AT gmail.com> To : Caml List <caml-list AT inria.fr>

: Caml List <caml-list AT inria.fr> Cc : Goswin von Brederlow <goswin-v-b AT web.de>

: Goswin von Brederlow <goswin-v-b AT web.de> Subject : Re: [Caml-list] GADTs and associative container

: Re: [Caml-list] GADTs and associative container Date: Wed, 10 Jul 2013 03:22:30 +0100

It is indeed possible to create associative containers where the typesof the values depend on the types of the keys. Let's see what can tobe done to turn the standard hash table into such a container, usingGADTs for keys.We'll start with the interface. The standard hash table interface(Hashtbl.S) looks like this, in part:module type S =sigtype keytype 'a tval create : int -> 'a tval add : 'a t -> key -> 'a -> unitval remove : 'a t -> key -> unitval find : 'a t -> key -> 'aval iter : (key -> 'a -> unit) -> 'a t -> unitendThe type of tables ('a t) is parameterized by the type of values,since each table holds a single type of value. We're aiming insteadto have value types depend on key types, so we'll move the typeparameter into the key type. Making this change mechanicallythroughout the interface gives us the following:module type GS =sigtype 'a keytype tval create : int -> tval add : t -> 'a key -> 'a -> unitval remove : t -> 'a key -> unitval find : t -> 'a key -> 'aval iter : < f: 'a. 'a key -> 'a -> unit > -> t -> unitendActually, I've made one additional change, in the type of iter. Inthe regular Hashtbl iter function we can get by with ML-stylepolymorphism, where all the type variables are implicitly quantifiedat the outermost point. This constrains the function passed to iterto be monomorphic, which is fine, since regular Hashtbls only supporta single value type. In our revised interface, however, the functionargument must be polymorphic, since it needs to handle *any* suitablepairing of keys and values. The object type allows quantifiernesting, giving us the polymorphism we need.The type of iter is a hint of things to come: putting things *into* apolymorphic hash table is a doddle, but there's a bit of a knack togetting them *out* again intact, as we'll see further down.Next up is the definition of keys. The standard Hashtbl.Make functoruses a definition of keys that bundles the key type together withequality and hashing operations, like this:module type HashedType =sigtype tval equal : t -> t -> boolval hash : t -> intendOf course, it's no good having just any old definitions of equal andhash. It's essential that equal l r implies hash l = hash r, forexample, and there are additional fairly obvious constraints on equal.Our analogue to HashedType, GHashedType, comes with some additionaloperations (and so places additional demands on the creator of hashtables). The first part of the signature looks essentially the sameas HashedType: we've added a parameter to the key type, but it's notused as yet, so we can replace it with the don't-care underscore.(Note that this means that our equality is heterogeneous, happy toaccept keys of disparate types.) The remainder of the signature dealswith packing up key-value pairs into existential boxes, and attemptingto get them out again; this will allow us to store different types ofkey in a single table in our implementation.module type GHashedType =sigtype _ keyval equal : _ key -> _ key -> boolval hash : _ key -> inttype pair = Pair : 'a key * 'a -> pairval unpack : 'a key -> pair -> 'aendAs with HashedType there are requirements not captured in the types.In particular, we'd like unpack k (Pair (k', v)) = v whenever equal kk' is true.Time for the implementation. This is mostly straightforward: aftersome preliminaries (mostly about hiding the type parameter in keys byboxing them up appropriately), there are two functions (add andremove) that put keys and values in boxes and store them in amonomorphic table, and two functions (find and iter) that unbox keysand values to recover the parameterization.module GHashtbl (G : GHashedType) :GS with type 'a key = 'a G.key =structinclude Gtype k = Key : 'a key -> kmodule H = Hashtbl.Make(structtype t = klet hash (Key k) = hash klet equal (Key l) (Key r) = equal l rend)type t = pair H.tlet create n = H.create nlet add tbl k v = H.add tbl (Key k) (Pair (k, v))let remove tbl k = H.remove tbl (Key k)let find tbl key = unpack key (H.find tbl (Key key))let iter (f : 'a -> unit>) tbl =H.iter (fun _ (Pair (k, v)) -> f#f k v) tblendAs is often the case, the unboxing is the interesting part. The findfunction reveals why we introduced the unpack operation for keys (andhence the pair type, which could otherwise have been hidden away inthe body of GHashtbl), and shows the secondary purpose of keys asunboxers of values. The iter function makes use of the polymorphismthat we introduced in its signature earlier; when we unbox a pair wehave no idea how to instantiate the type variable in the contents, soit's just as well we have a function to hand (f#f) that's polymorphicenough to handle any possible instantiation.Time to try it out. Here's a sample implementation of GHashedTypethat associates ints with int lists (which we'll use to store primefactors) and strings with bools (which we'll use to indicatecapitalization).module KeyType (* : GHashedType *) =structtype _ key =Int : int -> int list key| String : string -> bool keylet equal : type a b. a key -> b key -> bool =fun l r -> match l, r with| Int x, Int y -> x = y| String x, String y -> x = y| _ -> falselet hash = Hashtbl.hashtype pair = Pair : 'a key * 'a -> pairlet rec unpack : type a. a key -> pair -> a =fun k p -> match k, p with| Int _, Pair (Int _, v) -> v| String _, Pair (String _, v) -> v| _ -> raise Not_foundendUsing KeyType we can instantiate the functor and set about creatingheterogeneous hash tables:# module HT = GHashtbl(KeyType)...# let ht = HT.create 10;;val ht : HT.t = # begin HT.add ht (Int 10) [2; 5];HT.add ht (Int 12) [2; 2; 3];HT.add ht (Int 2) [2]; end;;- : unit = ()# begin HT.add ht (String "foo") false;HT.add ht (String "Foo") true;HT.add ht (String "bar") false;HT.add ht (String "Bar") true; end;;- : unit = ()# HT.find ht (Int 10), HT.find ht (String "Foo"), HT.find ht (Int 12);;- : int list * bool * int list = ([2; 5], true, [2; 2; 3])# let o = objectmethod f : type a. a key -> a -> unit =fun k v -> match k with| Int i -> let s = String.concat "*" (List.map string_of_int v) inPrintf.printf "%d = %s

" i s| String s -> Printf.printf "%s is%s capitalized

"s (if v then "" else " not")end;;val o : < f : 'a. 'a KeyType.key -> 'a -> unit > = # HT.iter o ht;;2 = 212 = 2*2*310 = 2*5foo is not capitalizedbar is not capitalizedBar is capitalizedFoo is capitalized- : unit = ()Jeremy.