OCaml: The Basics
Table of Contents
"You think you know when you can learn, are more sure when you can write, even more when you can teach, but certain when you can program."
-Alan J. Perlis, Epigrams in Programming (1982)
I'm of the opinion that, once you've learned one or two PLs, the process by which you learn new PLs transitions "course" to "crash course". Our goal here is not to give a comprehensive outline of OCaml. We're giving you enough to get started, then you'll have to practice and self-learn.
Also, in case we need another reminder, this is a PL course; as such we're gonna start approaching things more formally. That way when we start doing things capital-F-Formally the mental jump is shorter.
Built-in Data Types
OCaml has all the built-in data types that any reasonable PL has. This section is primarily for reference. The easiest way to familiarize yourself with built-in types is to use them.
Integers
Integers are values of type int.
According to the Int module: "Integers are Sys.int_size bits wide and
use two's complement representation. All operations are taken modulo
2Sys.int_size. They do not fail on overflow."
According to the documentation for Sys.int_size, it's an int that
represents the "[s]ize of int, in bits. It is 31 (resp. 63) when
using OCaml on a 32-bit (resp. 64-bit) platform."1
Literals are as usual (2, -345, and so on).2 and usual operators are available:3
+ |
integer addition |
- |
integer subtraction |
* |
integer multiplication |
/ |
integer division |
mod |
integer remainder |
abs |
integer absolute value (unary) |
Pretty standard stuff.
Floating-point numbers
Floating-point numbers are values of type float.
According to the Float module: "OCaml's floating-point numbers follow
the IEEE 754 standard, using double precision (64 bits)
numbers. Floating-point operations never raise an exception on
overflow, underflow, division by zero, etc. Instead, special IEEE
numbers are returned as appropriate, such as infinity for 1.0 /. 0.0,
neg_infinity for -1.0 /. 0.0, and nan ('not a number') for 0.0
/. 0.0. These special numbers then propagate through floating-point
computations as expected: for instance, 1.0 /. infinity is 0.0, basic
arithmetic operations (+., -., *., /.) with nan as an argument return
nan, …"
Short version: floating-point numbers are standard.
Literal as usual (2., -345.67, and so on).4
and the usual operators (among others) are available:
+. |
floating-point addition |
-. |
floating-point subtraction |
*. |
floating-point multiplication |
/. |
floating-point division |
** |
floating-point exponentiation |
abs_float |
floating-point absolute value (unary) |
sqrt |
square root (unary) |
ceil |
floating-point ceiling (unary) |
floor |
floating-point floor (unary) |
Remark. You've undoubtedly noticed at this point that we have different arithmetic operations for integers and floating points (e.g.,
+vs.+.). This has to do with the "really strongly typed" discussion in the previous set of notes. Expressions are typed, and they have their types in perpetuity. And(+)is a function5 ──a valid expression──and so it has a type, and it has that type in perpetuity. No function overloading, ever. So, yes, we need two addition functions.6
Booleans
Boolean values are of type bool.
In OCaml, the two boolean literals are true and false. And the
usual operators:
∣∣ |
disjunction |
&& |
conjunction |
not |
negation (unary) |
We'll also note here that OCaml has all the usual comparison operators:
= |
equal |
<> |
not equal |
< |
less than |
<= |
less than or equal to |
> |
greater than |
>= |
greater than or equal to |
Yes, equality is written with a single equals symbol. Yes, this means you can write confusing stuff like:
let foo = 1 = 2
No further comments.
Aside. But wait, hold on, what just happened. Didn't we just make a big hubbub about how an expression has it's type in perpetuity? Don't I need a different equality/inequality for integers and floats? Turns out: no. OCaml comparison operators a polymorphic. Exactly what this means will be a topic of later discussion, but in typically forward-reference you-get-the-gist fashion, the idea the type of all these comparison operators is:
'a -> 'a -> boolwhich we read this as: for any type
t, this function can be applied to twotvalues to get abool.And, how does this work? Once we learn more about parametric polymorphism, we might even be inclined to ask: how can this possibly work?7 Well, it's mostly magic, particularly when it comes to inequalities. Actually, polymorphic inequalities are a form of hidden function overloading.8 Which, yeah, not great.9 Folks have been discussing removing polymorphic comparison for years. I don't expect it'll ever disappear, and the truth is that it's really freakin' convenient (until it's not). We'll be using these comparison operators frequently, and not thinking about it too hard.
Strings (& Characters)
Strings are values of type string.
According to the String module: "A string s of length n is an
indexable and immutable sequence of n bytes. For historical reasons
these bytes are referred to as characters."
String literals are as usual ("foo", "bar", and so on).10
According to the Char module: "Characters are the elements of
string and bytes values. Characters represent bytes, that is an
integer in the range
[ 0x00 ; 0xFF ]."
Character literals are as usual ('b', 'a', 'r', and so on).11
The only real operations we have are concatenation (^) and indexing
(s.[i], where s is a string and i is an int).
More pretty standard stuff.
Aside. Except that string are one of the most complicated data types out there when you really get into it. There are thousands of languages and hundreds of writing systems12 all of whose basic elements need to be shoved into sequences of bytes if we want to use these languages for, say, variable names. With logographic writing systems, for example, doing basic things like determining word/morpheme boundaries becomes a whole lot more interesting. In a majority of introductory CS settings these details don't matter. But "language" is in the name of this course, so the nitty-gritty of strings matters more to us than in other areas.
That said, we're gonna ignore most of these details. We'll be working in English, which has a constant-sized alphabet, and we're gonna assume ASCII-encoding, no unicode, no emojis (bummer).
One last point: strings are decidedly not the same thing as lists of characters in OCaml. We haven't discussed lists yet so this point in fact be pointless, but we'll find that they're represented completely differently, are thus we'll work with them completely differently.
Unit
The humble unit. The literal () is of type unit. That's all
there is to it.
What's the point? We've already established OCaml is a functional PL and that's that. But also that's not it, because there are plenty of things we can/will often do in OCaml that are not functional, e.g., printing and asserting.
let _ = print_endline "Testing arithmetic..." let _ = assert (1 + 1 = 2)
print_endline is a function that writes to stdout. There isn't
really a value that we can naturally associate with the result of
printing. We see it, but from the perspective of the program,
nothing happened. In these "nothing happened" scenarios, we still
need a type. This is why the type of print_endline is:
string -> unit
It takes a string (prints it to stdout, unbeknownst to the program
itself) and then gives back (), basically as a placeholder to
satisfy the type system.
Expressions
So far we have literals for basic data types and a couple associated
operations. We're at about the point that we can use utop as a
replacement for our OS's calculator app. We need more language
constructs if we want a sufficiently powerful PL from a theoretical
perspective, and we'll need more than that if we want a usable PL.
First, a fundamental question: what is an expression really? We use this term frequently, e.g., when talking about arithmetic expression, or Boolean expression, but we rarely define the notion. Except in a PL course, that's kind of the whole deal.
A first approximation: expressions are syntactic objects that express
semantic objects. In other words, they're written-down things that
describe values. For example, the expression 1 + (2 - 3) describes
the value \(0\). It's not the same as \(0\), rather it evaluates to
\(0\).13 Thus expressions encode
implicit computation in the form of bunches of symbols. This is, in a
sense, the core of programming.
Remark. Expressions are not the same as statements. A statement is an imperative, it describes an action to be carried out. Most imperative languages you've worked with so far are statement-based. The fundamental construct is statement, and statements are combined to describe procedures. An example of a statement is an assignment statement, e.g., in Python:
x = 2 # assignment statement assert (x = 2) # assertion statement x = 3 + 4 # assignment statement assert (x = 7) # assertion statementAn assignment statement describes what value to assign
x. In the evaluation model of a PL like python, we think aboutxas a kind of abstract register and an assignment statement updates the value in that register.The major difference between expression and statements is that: expressions have values and statements do not. An assignment statement, for example, doesn't evaluate to anything, it just does a thing.
Also note that statement-based languages also have expressions, e.g., the right-hand-side of an assignment statement is an expression, which evaluates to the value to which the variable (on the left-hand-side) is assigned. But we cannot, for example, put a statement where an expression should go:
x = (y = 1 + 2) # SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?When working in a functional PL like OCaml, all we have are expressions (in particular, there is no notion of assignment, which depends on state). We write expressions, which implicitly describe computations, which are carried out during evaluation. Part of our job as PL designers and implementers is to make this computation explicit, by defining how each kind of expression works.
One more point. We've said an expression is a thing with a value. Another way of saying this is an expression is something with a type. This means, when we define how a part of the language works we have three jobs. We define the syntax, we define how they "work" and we define how we check their type.
Actually one more point. Expressions are defined recursively. This is
how we build more complex expressions. This is not new, but it may
not have been described to you in this way. The reason 1 + (2 - 3) *
4 is a well-formed arithmetic expression is that we can put any
arithmetic expression on either side of an operator. That's a
recursive definition; we've defined the structure of arithmetic
expressions in terms of the structure of arithmetic expressions.
Literals
Every literal we described above is an expression. We think of this as essentially the base case of our recursive definition.
Syntax. For example 1 and true and 3.45 and "six seven" are all
well-formed expressions.
Semantics. Every literal has a corresponding value. For example,
12 has the value \(12\). We will usually distinguish between the a
literal and its value by the font we use, except in the case of
Boolean values, for which we use \(\top\) for the true Boolean value an
\(\bot\) for the false Boolean value.
Typing. Every literal has the type described in its corresponding
section. For example, 12 has the type int.
If-expressions
Syntax. If \(e_1\), \(e_2\), and \(e_3\) are well-formed expressions, then so is \(\texttt{if} \ e_1 \ \texttt{then} \ e_2 \texttt{else} \ texttt{e_3}\).
We begin with conditionals. And as was hinted in our previous notes, this means specifying three things: syntax, typing and semantics. In what follows we'll specify these things informally, and put alongside the informal specification the formal ones as well as a sneak peak of what's to come in the second part of the course.
A quick reminder that OCaml is an expression based language. If-expressions are our first marked departure from the status quo of imperative languages which generally have if-statements. The difference between an expression and a statement is important, but somewhat subtle, and the kind of thing that we hope you appreciate coming out of this course. In broad strokes, a statement is an imperative; it describes a thing to do. Conditional statements are compound imperatives: given two imperatives, determine what to do based on the value of a boolean expression. In python we'd write something like:
if condition_to_check: # do something ... else: # do something else ...
An expression is a syntactic object that we evaluate to get a value. So the first major point: statements don't have values. Only expressions have values. What a lot of folks don't know is that Python also has if-expressions:
foo = then_expr if cond_to_check else else_expr
We won't spend this much time on the later constructs but I think this highlights two major points that get us into the expression-based mindset:
- It doesn't make sense to drop the else-case of an
if-expression. If the thing needs to evaluate to a value and the
condition is
false, we still need to give back something to evaluate.
Let-expressions
Functions
Applications
Additional Resources
Footnotes:
Curious, why do we lose a bit? This isn't terribly important to know, but if you're curious, the unaccounted for bit is called a tag bit and it's used by OCaml's garbage collector to distinguish integers from pointers, which saves it from doing unnecessary reachability analysis. See the chapter in memory representation in RWO if you're interested.
For the curious among y'all, see the lexical conventions of integer literals for the other conveniences regarding integer literals afforded by OCaml's grammar.
For more details on
how these work see the Int module documentation and the course
standard library documentation.
Check out the lexical conventions of floating-point literals for more unnecessary details.
Another subtlety here: infix operators can be made into expression by wrapping them in parentheses.
I believe this is better than having to remember on which types addition is defined and how it behave in each case, though I'm not so staunch on this point that I care terribly to convince you of my viewpoint.
In particular, a polymorphic function needs to be agnostic to its input, but we can't exactly think of integer comparison as being implemented by the same procedure as the one for list comparison (lists are compared lexicographically).
Tsk Tsk.
The situation is a little better w.r.t. equality; it boils down to structural equality, which doesn't require enforcing an ad hoc ordering.
More lovely unnecessary details.
You guessed it.
Source: I briefly Googled it.
This distinction is subtle, and takes some getting used to. We tend to say things like "1 + 2 is 3" but in reality the two objects in this phrase are quite different ontologically speaking. The first is syntactic, living the space of things we write down. The other is semantic, living in the Platonic realm of numbers (or in the registers of your CPU, if you're engineering-minded).