Power
Type Checking
Every OCaml expression has a type. If you have programmed in Java or C, this is a familiar concept. However, types become more important in a functional language like OCaml because of the focus on "expression-oriented" (ie: pure) computation as opposed to imperative computation that operates by assigning values to mutable variables and data structures. The OCaml type system is also substantially more precise that than the type systems of C or Java.
In order for an expression to be well-typed, the types expected by functions and operators
must coincide with the types of their arguments. For instance, the following expression
2 + 3
is well-typed because the +
operator expects a pair of integer arguments
and both 2
and 3
have type integer. On the other hand,
2.0 + 3.0
is not well-typed. The type of the +
operator does not change --
it continues to expect 2 integer arguments. However, 2.0
and 3.0
both have type float. If we type such an expression in to the toplevel, we will see the following
sort of error message.
# 2.0 + 3.0;; Characters 0-3: 2.0 + 3.0;; ^^^ Error: This expression has type float but an expression was expected of type int #The error message says that the plus operator expected an argument of type int, but the argument supplied has type float. As another example, suppose we enter our own add function in to the top-level and then supply a string argument:
# let add (x:int) (y:int) : int = x + y;; val add : int -> int -> int =# add (string_of_int 1) 2;; Characters 4-21: add (string_of_int 1) 2;; ^^^^^^^^^^^^^^^^^ Error: This expression has type string but an expression was expected of type int #
You will get a fair number of these as you start out. Do not despair! You will learn how to understand these messages and they will soon become a handy debugging aid. If you are having trouble figuring out the type of a sub-expression in your code, you can type it in to the OCaml toplevel to figure it out:
# string_of_int 1;; - : string = "1"Try this with functions too:
# string_of_int;; - : int -> string = < fun >The above says that
string_of_int
is a function from integers to strings.
One other technique is to add extra typing annotations to help you figure out the type
of an expression. You can add a typing annotation around any expression using the
syntax (exp : type)
. For instance:
# (2 + 3 : int) * 7;; - : int = 35
Type Checking Rules
Whether or not a expression type checks is governed by a uniform set of rules. Each different kind of expression has one rule for type checking it. For example, there is one rule for assigning a type to each different constant; there is one rule for assigning a type to an expression that uses a binary operator; there is one rule for assigning a type to an expression that applies a function to an argument; etc. One of the first steps in becoming a competent OCaml programmer involves memorizing these rules.
An important property of all type-checking rules is that they are compositional. This means that to type check an expression, one needs to know the types of its subexpressions, but nothing more. Said another way, the type of a subexpression serves as an effective interface for the subexpression -- when it comes to type checking, it tells you all you need to know.
When explaining the OCaml type checking rules,
we write exp : t
to mean "the expression
exp
is well typed and has type t
."
In the following paragraphs, we explain a few of the most important
type checking rules.
Integer Constants. Any integer constant n
has type int
. For example: 0 : int
and
17 : int
and -3 : int
.
There are similar rules for all other classes of constants such as
strings, characters and floating point numbers.
Binary Operators. Every binary operator
has already been assigned a type with the form
t1 -> t2 -> t3
(for some types t1
t2
and t3
). To find out the
type of an operator, just type it in to the OCaml toplevel
surrounded by parens ( ). For example:
# (+);; - : int -> int -> int =The last operator# (/.);; - : float -> float -> float = # (^);; - : string -> string -> string = # (@);; - : 'a list -> 'a list -> 'a list =
@
appends two lists together. It
happens to be a polymorphic operator
-- any type that begins with an apostrophe
(like 'a
) is a type variable that may be
instatiated with another type.
We'll learn more about polymorphic types later.
Now that we know that every binary operator op
has a type,
consider the expression exp1 op exp2
. The rule for type
checking such expressions is as follows:
if op : t1 -> t2 -> t3 and exp1 : t1 and exp2 : t2 then exp1 op exp2 : t3
As an example, consider the expression (2 + 3) * 5
.
To analyze this expression, we will start by analyzing the
subexpression 2 + 3
:
because + : int -> int -> int and 2 : int and 3 : int we conclude that 2 + 3 : intNow that we know the type of the subexpression
2 + 3
, we
can analyze the type of the entire expression:
because * : int -> int -> int and (2 + 3) : int and 5 : int we conclude that (2 + 3) * 5 : int
Function Application.
Consider an expression f exp
that applies the function
f
to the argument exp
. The typing rule
for such expressions is as follows.
if f : t1 -> t2 and exp : t1 then f exp : t2
As an example, consider the expression bool_of_string "true"
.
Checking in the OCaml toplevel, we discover the type of
bool_of_string
:
# bool_of_string;; - : string -> bool = < fun >With this knowledge, we analyze the expression as follows.
because bool_of_string : string -> bool and "true" : string we conclude that (bool_of_string "true") : bool
Consider next the function get
from the
String Module. This function retrieves the nth
character in
a string. For instance, (get "Dave") 3
returns the character
'e'
. How do we understand this expression in terms of
the type checking rules we know?
The type for get is
string -> int -> char
and since function types are
right associative, this type is equivalent to
string -> (int -> char)
. Here is the analysis:
because get : string -> (int -> char) and "Dave" : string we conclude that (get "Dave") : int -> charFurther:
because (get "Dave") : int -> char and 3 : int we conclude that ((get "Dave") 3) : charNote we typically write
(get "Dave") 3as
get "Dave" 3We may do this because functional application is left associative. The expressions with and without parentheses are equivalent.
Generally speaking, whenever we have a function
f : t1 -> t2 -> t3 -> ... -> tk
,
each time we apply an argument, we strip off
one of the argument types. Hence
f exp1
has type
t2 -> t3 -> ... -> tk
when exp1 : t1
and
f exp1 exp2
has type
t3 -> ... -> tk
.
When
exp1 : t1
and exp2 : t2
and ... and expk : tk
then
(f exp1 exp2 ... expk) : tk
.
Function Declaration. The rule for function declaration is straightforward. Given a function definition like this one:
let f (x1:t1) (x2:t2) ... (xk-1:tk-1) : tk = body_expThe type of the function is
t1 -> t2 -> ... -> tk-1 -> tk
provided that body_exp
has
type tk
under the assumption that
variables have their given types:
(x1:t1)
and (x2:t2)
and ... and (xk-1:tk-1)
.
As an example, consider test_squares
defined
as follows.
let test_squares (x:int) (y:int) : bool = x*x + y*y > 10
test_squares : int -> int -> bool
since its body has type bool
Let Declaration. We can also use let declarations to introduce names for other kinds of values (strings, numbers, fancier data structures, etc). In general, such let declarations look like this one:
let x : t = body_expThis expression is well-typed if
body_exp
has
type t
-- the type
declared for the variable x
. By the way, the type parameter
is optional. Sometimes it is useful as documentation to make your
code easier to read, but it is not required. OCaml will infer the
type automatically for you if you leave it out. We could write, for
instance:
let x = 3instead of
let x : int = 3
Let Expressions.
A let expression let x = exp1 in exp2
is type-checked similarly to a let declaration.
The expression as a whole has a type t2
provided that
exp1 : t1
and also
that exp2 : t2
under the assumption that
x : t1
.
Let expressions allow you to define local variables that are only used in some context (like inside a function). Let declarations don't have the "in" part -- they appear at the top level and their scope continues for the rest of the file.
Exercise: Use the toplevel environment to discover which of the following expressions are well-typed and which are not. In this exercise, we have left off type annotations on declarations in several places -- OCaml will infer them for you. However, it is good style to put type annotations on your top-level declarations.
For those expressions that are not well-typed, figure out how to fix them. Observe and remember the error messages that the toplevel supplies you with.
let x = 17.0 in x + x;; let square (x:int) : int = x**2;; let add_squares x y = x*x + y*y;; add_squares (addsquares 2) (addsquares 3);; let y = string_of_int 7 in y;; let s = "my lucky number is: " ^ y;; let s' = s ^ '!';;