Lecture 2.1: Functions
Last time, we started to covered the basics of OCaml; Today we will learn all about functions.
Editing OCaml in VSCode
Let's create tst.ml in VSCode, and fill it with the following contents:
let x = 3
let y = x + 1
let _ =
(* Print the value of y *)
print_endline ("Value of y: " ^ string_of_int y)
;; separator is not required in ml files.
Also, see that comments are written in OCaml like (* ... *).
Now, hover over various variables to see their types. You should see, for example, that x has type int,
while "Value of y: " has type string. If you hover over +, you will see it has type int -> int -> int; this is the type of a function that takes two integers and returns an integer. You will also see some helpful documentation.
Similarly, if you hover over string_of_int, you will see it has type int -> string.
To run it, we have a few options:
- Use
ocamlcto compile it: - Interpret it using
utop: - Load it using
utop, which will make the bindings available in the REPL:
Functions
So far, we have seen the basic OCaml types int, bool, and string. Now, let's look at functions. OCaml, being a functional programming language, makes functions very easy to use and manipulate.
To create a function, we can use the syntax fun (x : t) -> e, where t is a type and e is an expression:
Type annotations
We will see later in the class that type annotations can be optional. For now, we will use them throughout.
The value fun (x : int) -> x + 1 is an OCaml value, just as 3 or "hi" is. However, unlike int or string, we get a function type, written t1 -> t2, where t1 and t2 are types.
The <fun> syntax is OCaml's way of printing the value of the function.
Functions can be applied to arguments by writing them next to each other (and inserting parentheses where needed):
Note that we needed to wrap the fuction in parentheses; if we didn't, we would get a type error:
utop # fun (x : int) -> x + 1 3;;
Error: This expression has type int
This is not a function; it cannot be applied.
This is because OCaml would parse this as fun x : int -> x + (1 3), and this would mean treating 1 as a function and applying it to the argument 3.
Because functions are values, we can manipulate them just like other values, including let-binding them:
utop # let my_fun =
let f = fun (x : int) -> x + 1 in
let g = fun (y : int) -> f y + 1 in
fun (z : int) -> g (g z);;
val my_fun : int -> int = <fun>
utop # my_fun 2;;
- : int = 6
Multi-argument functions
Because functions are not special in OCaml relative to other types (e.g., integers or strings), they can return other functions:
Here,f has type int -> int -> int. This should be read as int -> (int -> int) (i.e., -> is right associative); in other words, f is a function which, given an int, returns a function of type int -> int.
Instead of thinking of f as a function which returns a function, we can also think of it as a function which takes two arguments. Indeed, f's declaration can be abbreviated as follows:
Additionally, an alternative (often more convenient) syntax for writing functions is as follows:
All three of the above declarations for f are equivalent.
This style of writing functions also works with multiple arguments:
Annotating return types
When you write functions like this, you can annotate the return type:
You can do this with regular values, too:
Partial Application
Since multi-argument functions in OCaml are just functions which return functions, we can partially apply them:
utop # let f (x : int) (y : int) = x + y;;
val f : int -> int -> int = <fun>
utop # let add_two = f 2;;
val add_two : int -> int = <fun>
utop # add_two 8 ;;
val - : int = 10
Higher-order functions
Functions can also be higher-order, meaning they can take functions as inputs (because function types are treated the same as any other type):
utop # let f (g : int -> int) = g 2;;
val f : (int -> int) -> int = <fun>
utop # f (fun (x : int) -> x * 5);;
- : int = 10
Note that f's type is (int -> int) -> int; this is a single argument function that takes a function of type int -> int as its argument. This is different than int -> int -> int, which takes two arguments of type int.
Exercise
What will this evaluate to?
Exercise
What will this evaluate to?
Specifying a "maximum" function
Already with the ingredients that we have --- namely, higher-order functions --- we can already start to study what it means to write a specification. We will do so with a case study, a three-valued version of max:
- How do we know we got it right?
- The specification in the comment is pretty unambiguous. But given this implementation..
how do we know it is right? We could manually inspect it, but that won't work all the time (think about
let max3 (i : int) (j : int) (k : int) = if i >= j then if i >= k then i else k else if j >= k then j else kmax4!), and is really easy to get wrong. - Instead, we want to implement the comment specification as a piece of formal logic, implemented in OCaml. How would we do so?
- First, before trying to implement it, let's write down the mathematical property
max3should have.
\(\forall i, \forall j, \forall k, \mathsf{max3}(i, j, k) >= i \wedge \mathsf{max3}(i, j, k) >= j \wedge\mathsf{max3}(i, j, k) >= k.\)
Is this correct? Is this sufficient? In other words, we are looking for a formula that encodes our intuition. If there is a solution for \(\mathsf{max3}\) that is different than what we actually want, that means there's a problem with the formula. I claim there are functions that make the above formula true, but you wouldn't count as a valid version.
Here's a fixed version: \(\forall i, \forall j, \forall k, \mathsf{max3}(i, j, k) >= i \wedge \mathsf{max3}(i, j, k) >= j \wedge\mathsf{max3}(i, j, k) >= k \wedge \mathsf{max3}(i, j, k) \in \{i,j,k\}.\)
- Next, we can implement the property. How could we turn this into a piece of OCaml code?