Weak Type Variables
Polymorphism and type variables
All terms of a well-typed program must have a type. The type of a term
denotes in which context the term could be used. For example, a
program term 42
has type int
and could be used anywhere where
int
is accepted.
If a term could be used in several different typing contexts (i.e.,
when the type of the term varies), then it is denoted by a type
variable in the type of the term. For example, 'a list
denotes a
term that fits into any place where some list is expected, e.g., int
list
, float list
, etc. We call such types polymorphic, as opposed
to monomorphic types. (Monomorphic types are sometimes called
ground types. These are types without any type variables.)
In OCaml, the type of a term is inferred from its definition. OCaml
tries to find the most unrestrictive type, i.e., the most general
type. For example, a term fun (x,y) -> (y,x)
has type ('a * 'b) ->
('b * 'a)
. That is, a term that accepts a term that is a pair of
any two types, and swaps them.
Why so weak?
Problems arise when we have to deal with mutable values.
In pure languages without mutability, we can treat values as pure mathematical objects. This is nice, because all the properties of a mathematical object are fully defined by its structure. So, our knowledge is static and there is no time variation involved.
In not so pure languages, i.e., in languages that support mutability, a mutable value is represented as a cell of memory, which is a placeholder. An assignment places a real value into the cell. (This real value could also be a cell by itself, but let’s not complicate things any further by discussing that now.) We can’t allow the type of a value in the cell to change with time, as in a statically typed programming language the type of a term is static, i.e., the type cannot change with time.
Since the well-typed property is static, the typechecker can’t claim that a program will be well-typed until some moment in the time, when someone puts a value of a wrong type into a cell and breaks it. In other words, there should not be any temporalities in the typechecker judgments, if a program may break, then the pessimistic assumption is that it will break. Of course since OCaml is a statically typed language, it doesn’t allow a reference to change its type over the time, so after we put a value of some type into a cell, the cell type is sealed.
But, what if we put into the cell a value that can have several types, i.e., a value that has polymorphic type? Which type should the compiler select?
For example, let’s consider the following simple program:
let x = ref None
The None
value is pure and has type 'a option
. The ref
value is also pure, and has type 'a -> 'a ref
. However, the 'a ref
type has the following representation:
type 'a ref = {mutable contents : 'a}
So which ground type should the compiler give to the value x
? The
value we have asked to put into the reference has type 'a option
,
which denotes a whole family of types, among which no type is better
than another.
Since compiler can’t make any choice here, it denotes it by weakening
the type variable, and ascribing to x
the following type '_a
option
. (Modern versions of OCaml use '_weak1
to make it more
noticable and explicit.)
Although '_a
is called a “weak type variable”, it is not actually a
variable, but rather an unknown type. What the compiler is trying to
say is: “this type must be monomorphic, because it is mutable, but I
can’t infer its type yet, because I haven’t yet collected enough
information.” Later uses of x
may provide more insights to the
compiler, and the weak type variable will then be transformed to a
ground type. For example, x := Some 42
will say to the compiler that
'_a
is actually int
.
This gives us yet another insight into what a weak type variable is — it is a delayed concrete type. However, the compiler may not delay the decision indefinitely, it must determine a ground type before the end of the file, a.k.a. the compilation unit. Indeed, because OCaml allows for separate compilation, other files cannot provide information to infer the type of a variable defined in our file.
If the compiler cannot find enough evidence for the ground type of the weak type variable before the end of the file, then an error is signaled, and we need to intervene and manually ascribe ground types for the weak type variable. This the rare case where a type annotation is actually required in OCaml.
Why does my immutable value have a weak polymorphic type?
Now it is clear why references and other mutable values can’t have polymorphic type, and why weak variables arise if the compiler can’t infer the ground type for a mutable value.
This is a reasonable limitation and a fair tradeoff — if you need a mutable cell, then you may need to type it manually. (As a courtesy, the compiler may type it for you if it can.)
But unfortunately, this is only the start of the story, as compiler may give weak types to terms that do not involve any mutable values. Consider the following example. Suppose we have a function:
let const x y = y
Why does const ()
have type '_a -> '_a
?
The problem is that const ()
is a partial application. The result of
a function application is a runtime value called a closure. Since
this value will be created only at runtime, the typechecker can’t
inspect it, so it is left beyond its reach. The typechecker needs to
be very pessimistic (if not paranoid!) and must assume that the
closure may create arbitrary references and could use them to breach
type soundness.
And in this case, it is not overcautious, as we can indeed breach type safety, as we show below:
let const _ =
let cache = ref None in
fun x -> match !cache with
| Some cached when cached = x -> cached
| _ -> cache := Some x; x
This function has the same type as the benign const
function, 'a ->
'b -> 'b
. However, its behavior is quite different. This function adds
a sort of hash-consing/memoization capability. It creates an identity
function that uses a reference for storage, and, if the last value is
structurally equal to the current value, the last value is
returned.
Let’s suppose that the typechecker were more lax, and gave the
type 'a -> 'a
to function id
in let id = const ()
.
Then this function could be applied as follows: id []; id None
.
The first application of id
will cache the empty list, while the
second application will compare an empty list with None
, and this
will breach the type system.
Even worse, passing references to this function will establish unsafe value aliasing, e.g.:
let x = ref 0 and y = ref 3.14 in
id x;
id y := 0.0
will cause a segmentation fault.
Fortunately, nothing like this will happen in the real world, as the
OCaml typechecker will give the weak polymorphic type '_a -> '_a
to
the identity function created by the partial application to
const
.
And, although fun _ x -> x
is totally benign, it has the same type
as the cached version of const
, so to the typechecker they are
indistinguishable.
This puts partial applications in the same category as references, arrays, hashtables, and other program constructs that the type checker must suspect of impurity.
As for the origin of the name “value restriction”: computer scientists decided to specify a class of program terms that are always pure, and to require all other terms to have monomorphic type and to weaken all type variables associated with them. They defined the class of pure terms syntactically, and called it values. Hence the name value restriction.
The word value here has nothing to do with the runtime notion of a value. Unfortunately this is an example when locally defined terminology leaks out of the context of its definition and wreaks havoc around the world.
In the context of the type systems, the word “value” corresponds to a syntactic class of non-reducible terms, or in other words, to syntactic constructs that are defined on the source level, i.e., static constants. So a better name would be “the static restriction”, that allows only statically defined terms to have polymoprhic type.
But, unfortunately, “the value restriction” is the historic name, so we’re stuck with it.
Relaxed Value Restriction
The value restriction was the first and rather pessimistic restriction
on value polymorphism, as it allowed only static constants to have a
polymorphic type, e.g. values such as []
, None
, fun x ->
x
, while all other values were considered possibly impure.
The good news is that in OCaml, this restriction is relaxed. Besides syntactic constants, OCaml allows some other classes of expressions to be generalized (i.e., to have polymoprhic type).
First of all, OCaml allows polymorphism for non-expansive expressions, where an expression is non-expansive if it doesn’t have any observable side-effects during evaluation. That extends the notion of a constant from the purely syntactic definition (something that looks like a constant is a constant) to a semantic definition (something that acts like a constant is a constant).
For example, this rule allows the following expressions to have
polymorphic type: lazy None
, let _ = ref None in 1
, and even
assert false
.
However, the expression (fun _ _ -> 0) ()
will be given a
non-polymorphic type '_a -> unit
, since the function fun _ _ -> 0
could perform some observable side-effect (it doesn’t, but the
typechecker doesn’t look into the body of a function).
Subtyping???
Now comes the most confusing part. The final relaxation of the value restriction is allowing the generalization of covariant type variables.
So how did this happen? Why would subtyping have any relationship to the value restriction? Do OCaml objects break polymorphism and complicate things even more? No, it is nothing like this. In fact, it has nothing to do with the OCaml object system and with the way how objects are typed in OCaml.
Before we delve into our explanation, we need to build some intuition about variance. Most of the articles are of no help for us, as they focus on subtyping, and it is really hard to connect subtyping with the value restriction. So let’s forget about subtyping and focus on functions.
In a function type, a type variable is covariant if it occurs only to the right of the arrow. If a variable occurs only to the left of the arrow, then it is contravariant. Otherwise, a variable is invariant. That is all that we need to know about variance in to build the right intuition to understand why it matters for the value restriction.
In OCaml it is possible to specify variance of a type variable:
covariant variables are denoted with the prefix +
, and contravariant
are denoted with the -
prefix, e.g., type (-'a,+'b) fn = 'a ->
'b
.
The OCaml typechecker will infer the variance automatically if the
type definition is available. For example, for the 'a -> 'b
type
OCaml will know without any further ado that 'a
is contravariant and
'b
is covariant. But, if a type is abstract, then the only way to
preserve this information is to use the above type annotations. (It is
also useful to play with variance annotations in the toplevel, to gain
intuition.)
So, let’s go back to the value restriction. The intuition behind a
covariant type is that values of this type are never consumed, but
only produced (e.g., unit -> 'a
, int -> 'a
, etc). Thus if a
computation produces a value that is polymorphic in some type 'a
which is covariant, then it is guaranteed that values of this type
were never stored, captured, or otherwise abused during the
computation. Thus it is safe to ascribe a polymorphic type to this
value.
Since OCaml has variance inference, this usually works out of box,
e.g., given a function val f : unit -> unit -> 'a
a partial
application f ()
will have polymorphic type unit -> 'a
. However,
if we will decide to abstact unit -> 'a
as
module Thunk : sig
type 'a t
val create : unit -> 'a t
end = struct
type 'a t = unit -> 'a
let create () = fun () -> assert false
end
Then semantically, the same code will suddenly get a weak type again:
Thunk.create ();;
- : '_a Thunk.t = <abstr>
The reason is that since we hid the type definition, OCaml can’t infer
variance, and must pessimistically assume that 'a
is invariant in
'a Thunk.t
. We can help the typechecker with a variance annotation
in module signature:
module Thunk : sig
type +'a t
val create : unit -> 'a t
end = struct
type 'a t = unit -> 'a
let create () = fun () -> assert false
end
and now, everyone is happy:
# Thunk.create ();;
- : 'a Thunk.t = <abstr>
Conclusion
The value restriction is a pessimistic assumption that compilers must make in the face of the possible mutability, and it is a price that must be payed even if mutability is not used.
Weak type variables alleviate the problem a little, by allowing us to
omit type annotations for values that don’t have concrete or
polymorphic type. However, since types are analyzed at the compilation
unit level (i.e., the file level), weak type variables can’t escape
the scope of a compilation unit (the scope of a file). So it is not
possible to have a weak type variable in a module interface (e.g., in
the mli
file). The compiler will reject all toplevel definitions
that have weak types, unless they are either hidden from the module
interface (i.e., omitted in the interface, for example, when the mli
file is empty) or given a concrete type.
If for some reason the compiler refuses to generalize your expression and weak type variables occur in your type, then you can try the following options to fix the situation:
- Convert your expression to a syntactic constant, by making it a
function, aka eta-expansion. E.g.,
let x = ref None
could be made polymorphic by makingx
a function, e.g.,let x () = ref None
- If the problematic type is abstract, then the compiler can’t perform
variance analysis on it. You can expose that some type variables
of your abstract type are covariant by making variance
annotations. For example,
type +'a t
in the signature will tell the compiler that values of that type could be safely given a polymorphic type. (Of course, the definition must correspond to the specification that is checked by the compiler. For example,type +'a t = 'a list
would be accepted by the type checker, wheretype +'a t = 'a ref
will be rejected.) - If nothing else helps, then you need to manually annotate your value with a concrete type and/or hide it from the interface (by adding an mli file).
Further reading
- Side effects and weak polymorphism in Real World OCaml,
- Section 5.1 Weak polymorphism and mutation in the OCaml Manual,
- Common Error Messages at ocaml.org,
- “My function is not polymorphic (enough) ?” and subsequent sections in “Frequently asked Questions about Caml”.
- Relaxing the Value Restriction in OCaml