Previous Contents Next
Chapter 2 Labels and variants

(Chapter written by Jacques Garrigue)



This chapter gives an overview of the new features in Objective Caml 3: labels, and polymorphic variants.

2.1 Labels

If you have a look at the standard library, you will see that function types have annotations you did not see in the functions you defined yourself.

#List.map;;
- : f:('a -> 'b) -> 'a list -> 'b list = <fun>
 
#String.sub;;
- : string -> pos:int -> len:int -> string = <fun>
Such annotations of the form name: are called labels. They are meant to document the code, and allow more checking where needed. You can simply add them in interfaces, just like they appear in the above types; but you can also give names to arguments in your programs, by prefixing them with a tilde ~.

#let f ~x ~y = x - y;;
val f : x:int -> y:int -> int = <fun>
 
#let x = 3 and y = 2 in f ~x ~y;;
- : int = 1
When you want to use distinct names for the variable and the label appearing in the type, you can use a naming label of the form ~name:. This also applies when the argument is not a variable.

#let f ~x:x1 ~y:y1 = x1 - y1;;
val f : x:int -> y:int -> int = <fun>
 
#f ~x:3 ~y:2;;
- : int = 1
Labels obey the same rules as other identifiers in Caml, that is you cannot use a reserved keyword (like in or to) as label.

2.1.1 Classic mode

In Objective Caml, there are two ways of using labels, either the default classic mode, or the commuting label mode.

You need do nothing special to be in classic mode, and legacy programs written for previous versions of Objective Caml will work with no modifications in this mode. Indeed, all the first chapter was written in this mode.

In the classic mode, labels need not be explicitly written in function applications, but whenever they are given they are checked against the labels in the function type.

#f 3 2;;
- : int = 1
 
#f ~x:3 ~z:2;;
Expecting function has type y:int -> int
This argument cannot be applied with label z
The above error message gives the the type of the function applied to its previous arguments (here x), and the position of the unexpected argument.

Similar processing is done for functions defined inside an application. If you define inline a function with labels, they are checked against the labels expected by the enclosing function.

#Hashtbl.iter;;
- : f:(key:'a -> data:'b -> unit) -> ('a, 'b) Hashtbl.t -> unit = <fun>
 
#let print_all tbl =
   Hashtbl.iter ~f:(fun ~key ~data -> Printf.printf "%s: %s\n" key data) tbl;;
val print_all : (string, string) Hashtbl.t -> unit = <fun>
 
#let print_all tbl =
   Hashtbl.iter ~f:(fun ~data ~key -> Printf.printf "%s: %s\n" key data) tbl;;
This function should have type key:'a -> data:'b -> unit
but its first argument is labeled data
2.1.2 Optional arguments

An interesting feature of labeled arguments is that they can be made optional. For optional parameters, the question mark ? replaces the tilde ~ of non-optional ones, and the label is also prefixed by ? in the function type. Default values may be given for such optional parameters.

#let bump ?(step = 1) x = x + step;;
val bump : ?step:int -> int -> int = <fun>
 
#bump 2;;
- : int = 3
 
#bump ~step:3 2;;
- : int = 5
A function taking some optional arguments must also take at least one non-labeled argument. This is because the criterion for deciding whether an optional has been omitted is the application on a non-labeled argument appearing after this optional argument in the function type.

#let test ?(x = 0) ?(y = 0) () ?(z = 0) () = (x, y, z);;
val test : ?x:int -> ?y:int -> unit -> ?z:int -> unit -> int * int * int =
  <fun>
 
#test ();;
- : ?z:int -> unit -> int * int * int = <fun>
 
#test ~x:2 () ~z:3 ();;
- : int * int * int = 2, 0, 3
Optional arguments behave similarly in classic and commuting label mode. Omitting the label of an optional argument is not allowed, and in both cases commutation between differently labeled optional arguments may occur.

#test ~y:2 ~x:3 () ();;
- : int * int * int = 3, 2, 0
Optional arguments are actually implemented as option types. If you do not give a default value, you have access to their internal representation, type 'a option = None | Some of 'a. You can then provide different behaviors when an argument is present or not.

#let bump ?step x =
   match step with
   | None -> x * 2
   | Some y -> x + y
 ;;
val bump : ?step:int -> int -> int = <fun>
It may also be useful to relay a functional argument from a function call to another. This can be done by prefixing the applied argument with ?. This question mark disables the wrapping of optional argument in an option type.

#let test2 ?x ?y () = test ?x ?y () ();;
val test2 : ?x:int -> ?y:int -> unit -> int * int * int = <fun>
 
#test2 ?x:None;;
- : ?y:int -> unit -> int * int * int = <fun>
2.1.3 Labels and type inference

While they provide an increased comfort for writing function applications, labels and optional arguments have the pitfall that they cannot be inferred as completely as the rest of the language.

You can see it in the following example.
#let bump_it bump x =
   bump ~step:2 x;;
val bump_it : (step:int -> 'a -> 'b) -> 'a -> 'b = <fun>
 
#bump_it bump 1;;
This expression has type ?step:int -> int -> int but is here used with type
  int -> int -> 'a
While we intended the argument bump to be of type ?step:int -> int -> int, it is inferred as step:int -> 'a -> 'b. These two types being incompatible (internally normal and optional arguments are different), a type error occurs when applying bump_it to the real bump.

We will not try here to explain in detail how type inference works. One must just understand that there is not enough information in the above program to deduce the correct type of bump. That is, there is no way to know whether an argument is optional or not, or which is the correct order (for commuting label mode), by looking only at how a function is applied. The strategy used by the compiler is to assume that there are no optional arguments, and that applications are done in the right order.

The right way to solve this problem is to add a type annotation to the argument bump.
#let bump_it (bump : ?step:int -> int -> int) x =
   bump ~step:2 x;;
val bump_it : (?step:int -> int -> int) -> int -> int = <fun>
 
#bump_it bump 1;;
- : int = 3
In practive, such problems appear mostly when using objects whose methods have optional arguments, so that writing the type of object arguments is often a good idea.

Normally the compiler generates a type error if you attempt to pass to a function a parameter whose type is different from the expected one. However, in the specific case where the expected type is a non-labeled function type, and the argument is a function expecting optional parameters, the compiler will attempt to transform the argument to have it match the expected type, by passing None for all optional parameters.

#let twice f (x : int) = f(f x);;
val twice : (int -> int) -> int -> int = <fun>
 
#twice bump 2;;
- : int = 8
This transformation is coherent with the intended semantics, including side-effects. That is, if the application of optional parameters shall produce side-effects, these are delayed until the received function is really applied to an argument.

2.1.4 Commuting label mode

The commuting label mode allows a freer syntax, at the constraint that you must write all labels both in function definition and application, and that labels must match in all types.

If this is your first reading of this tutorial, or if you are satisfied with classic mode, you can probably skip the rest of this section. You need not know anything more on labels. In particular, you should not be bothered by the fact that some libraries are written in commuting label mode: the mode in which a library is written and the mode in which one uses it are completely independent.


You can switch to commuting label mode giving the -labels flag to the various Objective Caml compilers. At the toplevel, you can also switch from classic mode to commuting label mode, and back, with the #labels pragma.

##labels true;;
In commuting label mode, formal parameters and arguments are only matched according to their respective labels. This allows commuting arguments in applications. One can also partially apply a function on any argument, creating a new function of the remaining parameters.

#let f ~x ~y = x - y;;
val f : x:int -> y:int -> int = <fun>
 
#f ~y:2 ~x:3;;
- : int = 1
 
#List.fold_left [1;2;3] ~init:0 ~f:(+);;
- : int = 6
 
#List.fold_left ~init:0;;
- : f:(int -> 'a -> int) -> 'a list -> int = <fun>
Optional parameters may also commute with non-optional or unlabelled ones.
#test () () ~z:1 ~y:2 ~x:3;;
- : int * int * int = 3, 2, 1
As described in section 2.1.3, for out-of-order applications, the type of the function must be known previous to the application, otherwise an incompatible out-of-order type will be generated.

#let h g = g ~y:2 ~x:3;;
val h : (y:int -> x:int -> 'a) -> 'a = <fun>
 
#h f;;
This expression has type x:int -> y:int -> int but is here used with type
  y:int -> x:int -> 'a
If in a function several arguments bear the same label (or no label), they will not commute among themselves, and order matters. But they can still commute with other arguments.

#let hline ~x:x1 ~x:x2 ~y = (x1, x2, y);;
val hline : x:'a -> x:'b -> y:'c -> 'a * 'b * 'c = <fun>
 
#hline ~x:3 ~y:2 ~x:5;;
- : int * int * int = 3, 5, 2
2.1.5 Suggestions for labeling

Like for names, choosing labels for functions is not an easy task. A good labeling is a labeling which

We explain here the rules we applied when labeling Objective Caml libraries.

To speak in an ``object-oriented'' way, one can consider that each function has a main argument, its object, and other arguments related with its action, the parameters. To permit the combination of functions through functionals in commuting label mode, the object will not be labeled. Its role is clear by the function itself. The parameters are labeled with names reminding either of their nature or role. Best labels combine in their meaning nature and role. When this is not possible the role is to prefer, since the nature will often be given by the type itself. Obscure abbreviations should be avoided.
List.map : f:('a -> 'b) -> 'a list -> 'b list
output : out_channel -> buf:string -> pos:int -> len:int -> unit
When there are several objects of same nature and role, they are all left unlabeled.
List.iter2 : f:('a -> 'b -> 'c) -> 'a list -> 'b list -> unit
When there is no preferable object, all arguments are labeled.
Sys.rename : src:string -> dst:string -> unit
String.blit :
  src:string -> src_pos:int -> dst:string -> dst_pos:int -> len:int -> unit
However, when there is only one argument, it is often left unlabeled.
Format.open_hvbox : int -> unit
In the standard library, this principle also applies to functions of two or three arguments, as long as their role is clear from the function's name.

Here are some of the label names you will find throughout the libraries.



Label Meaning
f: a function to be applied
pos: a position in a string or array
len: a length
buf: a string used as buffer
src: the source of an operation
dst: the destination of an operation
cmp: a comparison function, e.g. Pervasives.compare
key: a value used as index
data: a value associated to an index
mode: an operation mode or a flag list
perm: file permissions
ms: a duration in milliseconds

All these are only suggestions, but one shall keep in mind that the choice of labels is essential for readability. Bizarre choices will make the program harder to maintain.

In the ideal, the right function name with right labels shall be enough to understand the function's meaning. Since one can get this information with OCamlBrowser or the ocaml toplevel, the documentation is only used when a more detailed specification is needed.

2.2 Polymorphic variants

Variants as presented in section 1.4 are a powerful tool to build data structures and algorithms. However they sometimes lack flexibility when used in modular programming. This is due to the fact every constructor reserves a name to be used with a unique type. On cannot use the same name in another type, or consider a value of some type to belong to some other type with more constructors.

With polymorphic variants, this original assumption is removed. That is, a variant tag does not belong to any type in particular, the type system will just check that it is an admissible value according to its use. You need not define a type before using a variant tag. A variant type will be inferred independently for each of its uses.

Basic use

In programs, polymorphic variants work like usual ones. You just have to prefix their names with a backquote character `.
#[`On; `Off];;
- : [> `On | `Off] list = [`On; `Off]
 
#`Number 1;;
- : [> `Number of int] = `Number 1
 
#let f = function `On -> 1 | `Off -> 0 | `Number n -> n;;
val f : [< `On | `Off | `Number of int] -> int = <fun>
 
#List.map ~f [`On; `Off];;
- : int list = [1; 0]
[>`Off|`On] list means that to match this list, you should at least be able to match `Off and `On, without argument. [<`On|`Off|`Number of int] means that f may be applied to `Off, `On (both without argument), or `Number n where n is an integer. The > and < inside the variant type shows that they may still be refined, either by defining more tags or allowing less. As such they contain an implicit type variable. Both variant types appearing only once in the type, the implicit type variables they constrain are not shown.

The above variant types were polymorphic, allowing further refinement. When writing type annotations, one will most often describe fixed variant types, that is types that can be no longer refined. This is also the case for type abbreviations. Such types do not contain < or >, but just an enumeration of the tags and their associated types, just like in a normal datatype definition. For conciseness, of is omitted in polymorphic variant types.
#type 'a vlist = [`Nil | `Cons of 'a * 'a vlist];;
type 'a vlist = [ `Nil | `Cons of 'a * 'a vlist]
 
#let rec map ~f : 'a vlist -> 'b vlist = function
   | `Nil -> `Nil
   | `Cons(a, l) -> `Cons(f a, map ~f l)
 ;;
val map : f:('a -> 'b) -> 'a vlist -> 'b vlist = <fun>
Advanced use

Type-checking polymorphic variants is a subtle thing, and some expressions may result in more complex type information.
#function `A -> `B | x -> x;;
- : ([< `B | `A | .. > `B] as 'a) -> 'a = <fun>
Here the .. means that we know that `A and `B may not have an argument, but there is no specified upper bound on the number of variant tags in this variant type. We know also that `B can appear in the result, and input and output types have to be kept equal because x is returned as is.

#let f1 = function `A x -> x = 1 | `B -> true | `C -> false
 let f2 = function `A x -> x = "a" | `B -> true ;;
val f1 : [< `A of int | `B | `C] -> bool = <fun>
val f2 : [< `A of string | `B] -> bool = <fun>
 
#let f x = f1 x && f2 x;;
val f : [< `A of string & int | `B] -> bool = <fun>
Here f1 and f2 both accept the variant tags `A and `B, but the argument of `A is int for f1 and string for f2. In f's type `C, only accepted by f1, disappears, but both argument types appear for `A as int & string. This means that if we pass the variant tag `A to f, its argument should be both int and string. Since there is no such value, f cannot be applied to `A, and `B is the only accepted input.

Even if a value has a fixed variant type, one can still give it a larger type through coercions. Coercions are normally written with both the source type and the destination type, but in simple cases the source type may be omitted.
#type 'a wlist = [`Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a];;
type 'a wlist = [ `Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a]
 
#let wlist_of_vlist  l = (l : 'a vlist :> 'a wlist);;
val wlist_of_vlist : 'a vlist -> 'a wlist = <fun>
 
#fun x -> (x :> [`A|`B|`C]);;
- : [< `A | `B | `C] -> [ `A | `B | `C] = <fun>
You may also selectively coerce values through pattern matching.
#let split_cases = function
   | `Nil | `Cons _ as x -> `A x
   | `Snoc _ as x -> `B x
 ;;
val split_cases :
  [< `Nil | `Cons of 'a | `Snoc of 'b] ->
  [> `A of [> `Nil | `Cons of 'a] | `B of [> `Snoc of 'b]] = <fun>
When an or-pattern composed of variant tags is wrapped inside an alias-pattern, the alias is given a type containing only the tags enumerated in the or-pattern. this allows for many useful idioms, like incremental definition of functions.

#let num x = `Num x
 let eval1 eval (`Num x) = x
 let rec eval x = eval1 eval x ;;
val num : 'a -> [> `Num of 'a] = <fun>
val eval1 : 'a -> [< `Num of 'b] -> 'b = <fun>
val eval : [< `Num of 'a] -> 'a = <fun>
 
#let plus x y = `Plus(x,y)
 let eval2 eval = function
   | `Plus(x,y) -> eval x + eval y
   | `Num _ as x -> eval1 eval x
 let rec eval x = eval2 eval x ;;
val plus : 'a -> 'b -> [> `Plus of 'a * 'b] = <fun>
val eval2 : ('a -> int) -> [< `Plus of 'a * 'a | `Num of int] -> int = <fun>
val eval : ([< `Plus of 'a * 'a | `Num of int] as 'a) -> int = <fun>
To make this even more confortable, you may use type definitions as abbreviations for or-patterns. That is, if you have defined type myvariant = [`Tag1 int | `Tag2 bool], then the pattern #myvariant is equivalent to writing (`Tag1(_ : int) | `Tag2(_ : bool)).

Such abbreviations may be used alone,
#let f = function
   | #myvariant -> "myvariant"
   | `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>
or combined with with aliases.
#let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";;
val g1 : [< `Tag1 of 'a | `Tag2 of 'b] -> string = <fun>
 
#let g = function
   | #myvariant as x -> g1 x
   | `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>

Previous Contents Next