Previous Next Contents

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;;
- : fun:('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 add them in interfaces, just like they appear in the above types, and also write them directly in your programs.

 
#let f x:x y:y = x - y;;
val f : x:int -> y:int -> int = <fun>
 
#f x:3 y:2;;
- : int = 1
In order to lighten the notations, the expression name: name, where name is either a pattern variable or an identifier, can be abbreviated in :name.

 
#let f :x :y = x - y;;
val f : x:int -> y:int -> int = <fun>
 
#let pred x = f :x y:1;;
val pred : int -> int = <fun>
Warning
Since the colon : is used inside labels, you must be careful to put spaces around it in type annotations. You must write (x : int) and not (x:int). You may omit the space before the colon in type definitions and interface declarations, like in val x: int.

2.1.1   Classic mode

In Objective Caml, there are two ways of using labels, either the default classic mode, or the modern 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, except for the problem mentioned in the above warning.

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.

 
#List.fold_left;;
- : fun:(acc:'a -> 'b -> 'a) -> acc:'a -> 'b list -> 'a = <fun>
 
#let sum = List.fold_left fun:(fun :acc x -> acc + x) acc:0;;
val sum : int list -> int = <fun>
 
#let sum = List.fold_left fun:(fun x :acc -> acc + x) acc:0;;
This function should have type 'a -> 'b
but its argument is labeled acc:

2.1.2   Modern mode

You can switch to modern mode giving the -modern flag to the various Objective Caml compilers. You can also switch from classic mode to modern mode, and back, with the #modern pragma.

 
##modern true;;
The modern 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.

In modern 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.

 
#f y:2 x:3;;
- : int = 1
 
#List.fold_left [1;2;3] acc:0 fun:(fun :acc x -> acc + x);;
- : int = 6
 
#List.fold_left [1;2;3];;
- : fun:(acc:'a -> int -> 'a) -> acc:'a -> 'a = <fun>
For such 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.3   Optional arguments

An interesting feature of labeled arguments is that they can be made optional. An optional parameter is prefixed by a question mark ? in the function definition, and it 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 modern mode. Omitting the label of an optional argument is not allowed, and in both cases commutation between differently labeled optional arguments may occur. However commutation between an optional argument and other labeled or non-labeled arguments is only allowed in modern mode.

 
#test y:2 () ();;
- : int * int * int = 0, 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.4   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 the standard library.

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 modern mode, the object will not be labeled. Its role is clear by the function itself. The parameters are labeled with keywords 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 : fun:('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 : fun:('a -> 'b -> 'c) -> 'a list -> 'b list -> unit
When there is no preferable object, all arguments are labeled.
Sys.rename : old:string -> new: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
Here are some of the label names you will find throughout the standard library.



Label Meaning
pos: a position in a list, string or array
len: a length
buf: a string used as buffer
src: the source of an operation
dst: the destination of an operation
fun: a function to be applied
pred: a boolean predicate
acc: an accumulator
to: an output channel
key: a value used as index
data: a value associated to an index
mode: an operation mode or a flag list
perm: file permissions

All these are only suggestions, but one shall keep in mind that the choice of labels is essential for readability. Omissions or bizarre choices will make the program difficult 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 int] = `Number 1
 
#let f = function `On -> 1 | `Off -> 0 | `Number n -> n;;
val f : [<`On|`Off|`Number int] -> int = <fun>
 
#List.map fun: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 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 'a * 'a vlist];;
type 'a vlist = [`Nil|`Cons 'a * 'a vlist]
 
#let rec map fun:f : 'a vlist -> 'b vlist = function
   | `Nil -> `Nil
   | `Cons(a, l) -> `Cons(f a, map fun:f l)
 ;;
val map : fun:('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 int|`B|`C] -> bool = <fun>
val f2 : [<`A string|`B] -> bool = <fun>
 
#let f x = f1 x && f2 x;;
val f : [<`A int & string|`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 'a * 'a wlist | `Snoc 'a wlist * 'a];;
type 'a wlist = [`Nil|`Cons 'a * 'a wlist|`Snoc '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 'a|`Snoc 'b] -> [>`A [>`Nil|`Cons 'a]|`B [>`Snoc '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 'a] = <fun>
val eval1 : 'a -> [<`Num 'b] -> 'b = <fun>
val eval : [<`Num '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 'a * 'b] = <fun>
val eval2 : ('a -> int) -> [<`Plus 'a * 'a|`Num int] -> int = <fun>
val eval : ([<`Plus 'a * 'a|`Num int] as 'a) -> int = <fun>

Previous Next Contents