let rec qsort(lst) =
match lst with
| [] -> []
| pivot::rest ->
let rec split(lst1, lst2, lst3) =
match lst1 with
| [] -> qsort(lst2) @ [pivot] @ qsort(lst3)
| x::xs -> if x <= pivot then split(xs, x::lst2, lst3)
else split(xs, lst2, x::lst3)
in split(rest, [], [])
We don't have to change much. We have to change the function header to have an
extra parameter that I called "less". We have to use that function instead of
testing whether x<=pivot. And we have to include it as a parameter for our two
recursive calls:
let rec qsort(less, lst) =
match lst with
| [] -> []
| pivot::rest ->
let rec split(lst1, lst2, lst3) =
match lst1 with
| [] -> qsort(less, lst2) @ [pivot] @ qsort(less, lst3)
| x::xs -> if less(x, pivot) then split(xs, x::lst2, lst3)
else split(xs, lst2, x::lst3)
in split(rest, [], [])
I set up a list we could use for testing and tried calling qsort passing it the
built-in less-than operator:
# let test = [17; 3; 42; 198; -5; 14; -7; 0; -203; -2; 6; 155];;
val test : int list = [17; 3; 42; 198; -5; 14; -7; 0; -203; -2; 6; 155]
# qsort((<), test);;
Error: This expression has type 'a * 'a -> 'a * 'a -> bool
but an expression was expected of type 'a * 'a -> bool
Type 'a * 'a -> bool is not compatible with type bool
This produced an error because we wrote qsort with the assumption that the
comparison function would take a tuple. The built-in less-than operator is
curried. Remember that our utility file has a function called uncurry that can
be used to convert a curried function into an uncurried one:
# qsort(uncurry(<), test);;
- : int list = [-203; -7; -5; -2; 0; 3; 6; 14; 17; 42; 155; 198]
# qsort(uncurry(>), test);;
- : int list = [198; 155; 42; 17; 14; 6; 3; 0; -2; -5; -7; -203]
Notice that we can sort in either ascending or descending order by using either
the standard less-than or the standard greater-than. We also can define an
anonymous function:
# qsort((fun (a, b) -> a mod 3 < b mod 3), test);;
- : int list = [-5; -2; -203; -7; 6; 3; 0; 42; 198; 17; 155; 14]
# qsort((fun (a, b) -> abs(a) < abs(b)), test);;
- : int list = [0; -2; 3; -5; 6; -7; 14; 17; 42; 155; 198; -203]
In the first case we are sorting by the value in mod 3 (with the multiples of 3
first, then the values 1 more than a multiple of 3, then the values 2 more than
a multiple of three). In the second call we use the abs function to allow us
to sort by absolute value.Then I asked people how to write a function that would return a list in reverse order. For example, the call reverse(1--4) should return [4; 3; 2; 1]. We use the familiar pattern match of an empty list and a non-empty list, but we had to think about what to do with the "x" at the front of the list we are processing. Someone suggested using the cons operator (::) to add it to the end of the list.
let rec reverse(lst) =
match lst with
| [] -> []
| x::xs -> reverse(xs)::x
The problem is that :: expects a value followed by a list and this has it in
the reverse order. Someone mentioned that we could use the append operator
instead:
let rec reverse(lst) =
match lst with
| [] -> []
| x::xs -> reverse(xs) @ [x]
This version works, but it is inefficient. To explain why, I spent a few
minutes discussing how the :: (cons) and @ (append) operators work in OCaml.Consider the following bindings:
let x = [1; 2; 3]
let y = 0::x
let z = x @ [4]
OCaml stores lists internally as a linked list of nodes that each have data and
a reference to the next node (these correspond to the head and the tail). So
the structure is similar to what we get with standard Java linked list
nodes:
public class ListNode {
public int data;
public ListNode next;
...
}
So when we execute:
let x = [1; 2; 3]
OCaml creates a list of 3 nodes:
x --> [1] --> [2] --> [3]
What happens when we execute the second binding?
let y = 0::x
Recall that the :: operator is referred to as "cons," which is short for
"construct." In other words, it constructs a new list element:
y --> [0] --> ??
This new list element has 0 as the head, but what does it use for the tail?
OCaml could make a copy of the list that x refers to, but that's not what
happens. instead, it sets up a link that shares the memory set aside for x:
x --> [1] --> [2] --> [3]
^
|
y --> [0] --+
In the Java universe this would be a bad idea. If you share part of the list,
then you're likely to end up with a confusing outcome. For example, what if
you use the variable y to traverse the list and you change the second and third
values in the list. The second and third values in the list that y refers to
are the first and second values in the list that x refers to. So in that case,
changing y would change x as well. Normally we'd want to avoid that kind of
interference.This is where the concept of mutable state comes into play. In OCaml, lists are immutable. Once you have constructed a list element with a particular head and tail, you can never change them. So it's not dangerous to allow this kind of sharing because OCaml prevents this kind of interference. This is an example where the choice to prevent mutation has made it easier for OCaml to be efficient about the allocation of space.
You can simulate this in Java as well. To get immutable lists in Java, you'd make the fields final:
public class ListNode {
public final int data;
public final ListNode next;
...
}
But what about the final binding?
let z = x @ [4]
This is a case where OCaml can't make use of sharing. The variable x refers to
a list that ends with 3. If you tried to change it to instead point to a new
list element storing 4, then you'd be damaging the original list. So this is a
case where OCaml has no choice but to make a copy of the contents of x:
z --> [1] --> [2] --> [3] --> [4]
A simple rule of thumb to remember is that the :: operator always executes in
O(1) time (constant time) because it always constructs exactly one list element
while the @ operator runs in O(n) time where n is the length of the first list
because that first list has to be copied.So let's return to the inefficient version of the reversing function:
let rec reverse(lst) =
match lst with
| [] -> []
| x::xs -> reverse(xs) @ [x]
Because we are using the @ operator, we are going to be creating lots of list
copies. In effect, to reverse a list of n elements, we're going to make a copy
of a list of length 1, and a copy of a list of length 2, and a copy of a list
of length 3, and so on, ending with making a copy of a list of length n-1.
That will require O(n2) time. We saw that when we made a call on
this version of reverse passing it a list with 10 thousand elements
(1--10000).We can do better than that. I asked people how you'd approach it iteratively. We came up with this pseudocode:
set result to an empty list
while (list we are reversing is not empty) {
x = remove first element of list we are reversing
add x to result list
}
You can translate an iterative process like this in a functional equivalent by
thinking about the different states that this computation goes through. There
are two different variables involved here: list and result. Here's how they
change as you iterate through the loop assuming we are working with the list
1--4.:
list result
----------------------------
[1; 2; 3; 4] []
[2; 3; 4] [1]
[3; 4] [2; 1]
[4] [3; 2; 1]
[] [4; 3; 2; 1]
Instead of having two variables that change in value each time you iterate
through the loop (the mutable state approach), you can instead have a function
of two arguments where each time you call the function you compute the next
pair of values to use in the computation. So we'll write this using a helper
function:
let reverse(lst) =
let rec helper(lst1, lst2) = ??
in helper(??)
The loop starts with list being the overall list and result being empty. In
the functional version, we make this the initial call on the helper
function:
let reverse(lst) =
let rec helper(lst1, lst2) = ??
in helper(lst, [])
The loop ends when list becomes empty, in which case the answer is stored in
result, so this becomes one of the cases for our helper function:
let reverse(lst) =
let rec helper(lst1, lst2) =
match lst1 with
| [] -> lst2
...
in helper(lst, [])
Now we just need a case for the other iterations. In the pseudocode, we pulled
an x off the front of the list and moved it to result. We can accomplish this
with a pattern of x::xs for the list and by moving x into the result in our
recursive call:
let reverse(lst) =
let rec helper(lst1, lst2) =
match lst1 with
| [] -> lst2
| x::xs -> helper(xs, x::lst2)
in helper(lst, [])
We saw that this version worked and ran in a reasonable amount of time even for
lists with a million values.Then we turned to a new topic: data types. We started by defining a type that corresponds to an enumerated type in Java, C, and C++. Suppose that you want to keep track of various colors and you'd like to have meaningful names for them. This is easy to do in OCaml:
type color = Red | Blue | Green
This definition introduces a new type called "color". We use the vertical bar
or pipe character ("|") to separate different possibilities for the type. This
type has three possible forms. OCaml refers to the three identifiers as
constructors, even though in this case they are very simple and don't require
any data. OCaml requires that type names start with a lowercase letter and
constructors start with an uppercase letter. You can ask about the
constructors in the interpreter:
# Red;;
- : color = Red
You can also write functions that use these identifiers, as in:
let rgb(c) =
match c with
| Red -> (255, 0, 0)
| Green -> (0, 255, 0)
| Blue -> (0, 0, 255)
This function returns a tuple of integers that correspond to standard RGB
sequences for a given color (three integers in the range of 0 to 255 that
represent the red, blue, and green components of each).I then turned to a more complex example. I said that I wanted to explore the definition of a binary search tree in OCaml. I asked people what binary trees look like and someone said that they can be empty or they have a node with left and right subtrees. This becomes the basis of our type definition:
type int_tree = Empty | Node of int * int_tree * int_tree
The name of the type is int_tree. It has two different forms. The first form
uses the constructor Empty and has no associated data. The second form uses
the constructor Node and takes a triple composed of the data for this node (an
int), the left subtree and the right subtree. Notice how the keyword "of" is
used to separate the constructor from the data type description.Given this definition, we could make an empty tree or a tree of one node simply by saying:
# Empty;;
- : int_tree = Empty
# Node(38, Empty, Empty);;
- : int_tree = Node (38, Empty, Empty)
Notice that we use parentheses to enclose the arguments to the Node
constructor. The Node constructor is similar to a function but has a slightly
different status, as we'll see. In particular, we can use constructors in
patterns, which makes our function definitions much clearer.For example, we wrote the following function to insert a value into a binary search tree of ints.
let rec insert(value, tree) =
match tree with
| Empty -> Node(value, Empty, Empty)
| Node(root, left, right) ->
if (value <= root) then Node(root, insert(value, left), right)
else Node(root, left, insert(value, right))
If we are asked to insert a value into an empty tree, we simply create a leaf
node with the value. Otherwise, we compare the value against the root and
either insert it into the left or right subtrees. In a language like Java, we
would think of the tree as being changed (mutated). In OCaml, we instead think
of returning a new tree that includes the new value.To insert a sequence of values, you can use list recursion calling the insert function repeatedly:
let rec insert_all(lst) =
match lst with
| [] -> Empty
| x::xs -> insert(x, insert_all(xs))
Then we wrote a function for finding the height of a tree. I mentioned that
I'm using a slightly different definition for the height of a tree. In the
usual definition, the empty tree has a height of -1. I prefer to define the
height of the empty tree as 0, so this is returning a count of the number of
levels in the tree:
let rec height(t) =
match t with
| Empty -> 0
| Node(root, left, right) -> max (height left) (height right) + 1
In writing this, we had to use parentheses slightly differently because the
built-in max function is a curried function. Notice how we follow max by two
parenthesized calls on height.I pointed out that we are not using the value of "root" (the data stored at the root). This is a good place to use an anonymous variable, which you indicate with an underscore:
let rec height(t) =
match t with
| Empty -> 0
| Node(_, left, right) -> max (height left) (height right) + 1
In the interpreter, I constructed a tree with a million random values and asked
for its height by saying:
let t = insert_all(random_numbers(100000))
height(t)
We found that the height was around 50 even though we haven't done anything
special to balance the tree.
The last topic we discussed was
tail recursion.
Recall that we wrote this version of the factorial function:
This extra variable used to keep track of the product is sometimes refered to
as an accumulator. Our efficient version of reverse used a similar approach to
build up the reversed list. So we would refer to that parameter as an
accumulator as well and it turns out that the more efficient version of reverse
is also tail recursive.
It is well known that tail recursive functions are easily written as a loop.
Functional languages like Scheme and OCaml optimize tail recursive calls by
internally executing them as if they were loops (which avoids generating a deep
stack of function calls).
I also mentioned that the versions of map, filter and reduce that I've shown
are not tail-recursive. The standard operators like List.map, List.filter,
List.fold_left and List.fold_right are written in a tail-recursive manner to
make them more efficient.
let rec factorial(n) =
if n = 0 then 1
else n * factorial(n - 1)
This version works, but it turns out that we can improve on its efficiency. If
we were writing this code in Java, we would say something like:
int product = 1;
for (int i = 1; i <= n; i++) {
product = product * i;
}
Several times I've tried to make the point that you can turn this kind of loop
code into a functional equivalent. If it was useful for the loop to have an
extra variable for storing the current product, then we can do the same thing
with a helper function. We can have a 2-argument function that keeps track of
the current product in addition to the value of i. Using that idea, I wrote
the following variation of factorial:
let factorial(n) =
let rec helper(n, result) =
match n with
| 0 -> result
| n -> helper(n - 1, result * n)
in helper(n, 1)
They both compute factorial(n) in a similar manner, but the second one is more
efficient. Think about what happens when we compute factorial(5) using the
first version:
factorial(5) =
5 * factorial(4) =
5 * 4 * factorial(3) =
5 * 4 * 3 * factorial(2) =
5 * 4 * 3 * 2 * factorial(1) =
5 * 4 * 3 * 2 * 1 * factorial(0) =
5 * 4 * 3 * 2 * 1 * 1 = 120
Notice how the computation expands as we make recursive calls. After we reach
the base case, we'll have a lot of computing left to do on the way back out.
But notice the pattern for the second version:
factorial(5) =
helper(5, 1) =
helper(4, 5) =
helper(3, 20) =
helper(2, 60) =
helper(1, 120) =
helper(0, 120) = 120
There is no expansion to the computation. The key thing to notice is that once
we reach the base case, we have the overall answer. There is no computation
left as we come back out of the recursive calls. This is a classic example of
tail recursion. By definition, a tail recursive function is one that performs
no additional computation after the base case is reached.
Stuart Reges
Last modified: Tue Feb 13 10:34:57 PST 2024