Let's Get Funky with the Camel
How many ways can we get Fun(ky)
In any programming paradigm, it’s critical to understand how we write functions - be they traditional imperative , anonymous , recursive, or functional. In this post, I will break down the different types of functions that you can write in OCaml.
Let’s start by examining the imperative function. Here’s a simple function that prints out the phrase Hello World! $n$ times, once on each line, and returns the value $n$ as its result.
let say_it_loudly n =
for i = 0 to n do
Printf.printf "Hello World, %d\n" (i + 1)
done;
n;;
Notice a few things. We don’t need to use a keyword to declare a function. Simply specifying a variable following the let keyword defines our function name. This function takes one parameter. Should we want to define a function which takes no parameters we would need to use the unit type ( )
. For example let say_it_loudly ()
. Next, check out the n
at the end of the function. Our function is comprised of two expressions separated by a semi-colon. By adding the n
expression to the end of the function definition we make the value of the function expression an int, the type of expression n
.
In fact, we see that the expression results in an int by the definition returned when creating the function.
val say_it_loudly : int -> int = <fun>
But what would our function expression evaluate to if we omitted the expression n
? A clue can be given by the value definition returned when we define the function say_it_loudly
only this time modified by omitting the n
expression. What do we see?
val say_it_loudly : int -> unit = <fun>
This time, we see that the expression evaluates to the unit type. If we assigned the value of the function to a variable, we’d get the unit type. By adding the n
expression at the end of the function definition, assigning a value to the function expression results in the value being assigned the value of n
in our function.
Mutating Values -
We need to remember that by default, all of our data types are immutable unless we explicitly make it mutable. To do so we can create a reference (or use the mutable keyword in a type definition). Take the following example below:
# let a = ref 42;;
val a : int ref = {contents = 42}
Here we define a reference variable, a
, and assign it’s value the initial value of $42$. We can retrieve the content stored in the reference by using the reference cell contents operator, !, and we can assign a new value to the reference using the reference cell assignment operator, :=.
# let b= !a;;
val b : int = 42
# a := (b * b);;
- : unit = ()
Here we assign b
to the value of the reference, a
. Then, we replace the value of a
with the square of b
. Two important points to keep in mind here. First, pay particular attention to the use of parentheses. We need to be judicious with the use of parentheses ensuring that the arguments to the expression is proper. In the example above, the parentheses aren’t required, but certain expressions will evaluate erroneously if the parentheses are absent.
Next, look at the return value of the reference assignment. You might expect to see the value of a
. But as this is a mutation of a
, the value is actually of type unit. If we wanted to have the expression evaluate to the value stored in a
after the replacement, we would need to use a semi-colon followed by a
.
# a:= (b * b);a;;
- : int ref = {contents = 1936}
If we failed to use the ! operator and assign a
(the reference) to b
, we would simply have two expressions, a
and b
, pointing to the same reference. That is to say, references are pointers in OCaml.
Anonymous Functions
We can define anonymous functions simply using the fun
keyword. For example, let’s define a function that takes an ordered pair of objects and returns the reverse pair. Notice, I didn’t say an ordered pair of integers or floats, or any other explicit type. Here we define a polymorphic function that works for virtually all types.
# fun (a,b) -> (b,a);;
- : 'a * 'b -> 'b * 'a = <fun>
The return value of the definition shows that we’ve defined a function that takes 'a * 'b
, the tuple of any two types and returns a new tuple but reversed. But we can’t use the function definition - it’s anonymous. To use it we either pass it to another function that expects an anonymous function of the type we’ve defined, or we can assign it to an expression and use it directly. Let’s assign the anonymous function to the name flip
.
# let flip =fun (a,b) -> (b,a);;
val flip : 'a * 'b -> 'b * 'a = <fun>
So let’s assign a tuple to a value and pass it to our function:
# let bag1 = ("dog", 42);;
val bag1 : string * int = ("dog", 42)
# flip bag1;;
- : int * string = (42, "dog")
Another aspect of function expressions and bindings it pattern-matching. Let’s say we want to deconstruct the tuple returned from flip
into the respective values. We could do the following:
# let (age, animal) = flip ("dog", 2);;
val age : int = 2
val animal : string = "dog"
A more practical example of using anonymous functions is demonstrated with the List.map
function. List.map takes two arguments, a function that a value of type ‘a and returns a value of type ‘a, and a list of values of type ‘a. Type ‘a?? What’s that?? We’ll that’s just a place-holder for any type. The function List.map
and map any type so long as the function and the list of elements are of the same types.
Let’s demonstrate this with numbers an strings. First integers. Say we have a list and we want to return a new list that is a multiple of 5 for each element in the list:
# List.map (fun x -> x * 5) [1;2;3];;
- : int list = [5; 10; 15]
Next, we’ll use the same function, but this time we’ll define an anonymous function that operates on strings. Let’s say we have a list of filenames without extensions, and we want to create a corresponding list of filenames with a specific extension:
# List.map (fun x -> x ^ ".txt") ["apples";"bananas"];;
- : string list = ["apples.txt"; "bananas.txt"]
Let’s expand on our example above and create a variation of the anonymous function. This time, let’s parameterize the function so we can pass the extension to the function. To do this, let’s assign our anonymous function a name with a parameter:
# let mkext ext = (fun x -> x ^ ext)
val mkext : string -> string -> string = <fun>
Notice what happens if we call this function outside of another function. Here, we call it passing the argument strings ".exe".
# mkext ".exe";;
- : string -> string = <fun>
Evaluating our function returns a function! We can name this function for example exec_ext
and use it in our List.map
function:
# List.map exec_ext ["apples";"bananas"];;
- : string list = ["apples.exe"; "bananas.exe"]
We could also use mkext
directly in our List.map
function as follows:
# List.map (mkext ".ml") ["apples";"bananas"];;
- : string list = ["apples.ml"; "bananas.ml"]
Here’s an example where the parentheses are required for the expression evaluation.
Recursive Functions
OCaml has an explicit keyword, rec, indicating that we are defining a recursive function. Here’s an example of the traditional factorial function defined recursively:
# let rec fact n =
if n <= 0 then
1
else
n * fact (n -1)
val fact : int -> int = <fun>
Explicit Function definition with Pattern-Matching and match
Yet another way to declare a function in OCaml is using the function
keyword. For example, we can create a function, is_even
that matches any value and returns the boolean expression equivalent to that value being an even number:
# let is_even = function
| x -> x mod 2 == 0
;;
val is_even : int -> bool = <fun>
# is_even 5;;
- : bool = false
We can omit the function
keyword an achieve a similar solution using the match/with
keywords.
# let is_odd num =
match num mod 2 with
| 1 -> true
| 0 -> false
;;
Characters 18-64:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
2
val is_odd : int -> bool = <fun>
We defined our function is_odd
using match/with
, but we received warning! The interpreter is complaining that the matching may not be exhaustive. In this example, the warning is indeed inaccurate as there can only be $0\text{ } \text{or}\text{ }1$ as the modulus of any number base $2$.
Final Thoughts
So there you have it! That’s a lot of what you can do with functions in OCaml. I didn’t cover labeled and optional function parameters - I’ll leave that exercise to the curious reader!
Up next, records and variants in OCaml… stay tuned!!
-- rob