Previous Up Next
B.1 Variant types


Variants are tagged unions, like ML datatypes. Thus, they allow values of different types to be mixed together in a collection by tagging them with variant labels; the values may be retrieved from the collection by inspecting their tags using pattern matching.

However, unlike datatypes, variants can be used without a preceding type declaration. Furthermore, while a datatype constructor belong to a unique datatype, a variant constructor may belong to any (open) variant.

Quick overview
Just like sum type constructors, variant tags must be capitalized, but they must also be prefixed by the back-quote character as follows:
      
let one = `Int 1 and half = `Float 0.5;;
val one : [> `Int of int] = `Int 1
val half : [> `Float of float] = `Float 0.5
Here, variable one is bound to a variant that is an integer value tagged with `Int. The > sign in the type [> `Int of int] means that one can actually be a assigned a super type. That is, values of this type can actually have another tag. However, if they have have tag `Int then they must carry integers. Thus, both one and half have compatible types and can be stored in the same collection:
      
let collection = [ onehalf ];;
val collection : [> `Int of int | `Float of float] list =
  [`Int 1; `Float 0.5]
Now, the type of collection is a list of values, that can be integers tagged with `Int or floating point values tagged with `Float, or values with another tag.

Values of a heterogeneous collection can be retrieved by pattern matching and then reified to their true type:
      
let float  = function
  | `Int x -> float_of_int x
  | `Float x -> x;;
val float : [< `Int of int | `Float of float] -> float = <fun>

      
let total =
  List.fold_left (fun x y -> x +. float y) 0. collection ;;

Implementing typecase with variant types
The language ML does not keep types at run time, hence there is no typecase construct to test the types of values at run time. The only solution available is to explicitly tag values with constructors. OCaml data types can be used for that purpose but variant types may be more convenient and more flexible here since their constructors do not have to be declared in advance, and their tagged values have all compatible types.

For instance, we consider one and two dimensional point classes and combine their objects together in a container.
      
class point1 x = object method getx = x + 0 end;;
let p1 = new point1 1;;
To make objects of the two classes compatible, we always tag them. However, we also keep the original object, so as to preserve direct access to the common interface.
      
let pp1 = p1, `Point1 p1;;
We provide testing and coercion functions for each class (these two functions could of also be merged):
      
exception Typecase;;
let is_point1 = function _, `Point1 q -> true | _ -> false;;
let to_point1 = function _, `Point1 q -> q | _ -> raise Typecase;;
as well as a safe (statically typed) coercion point1.
      
let as_point1 = function pq -> (pq :> point1 * _);;
Similarly, we define two-dimensional points and their auxiliary functions:
      
class point2 x y = object inherit point1 x method gety = y + 0 end;;
let p2 = new point2 2 2;;
let pp2  = (p2 :> point1), `Point2 p2;;
let is_point2 = function _, `Point2 q -> true | _ -> false;;
let to_point2 = function _, `Point2 q -> q | _ -> raise Typecase;;
let as_point2 = function pq -> (pq :> point2 * _);;
Finally, we check that objects of both classes can be collected together in a container.
      
let l =
  let ( @:: ) x y = (as_point1 x) :: y in
  pp1 @:: pp2 @:: [];;
Components that are common to all members of the collection can be accessed directly (without membership testing) using the first projection.
      
let getx p = (fst p)#getx;;
List.map getx l;;
Conversely, other components must accessed selectively via the second projection and using membership and conversion functions:
      
let gety p =  if is_point2 p then (to_point2 p) # gety else 0;;
List.map gety l;;


Previous Up Next