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
-
makes programs more readable,
- is easy to remember,
- when possible, allows useful partial applications.
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>