- Two cases
- One or more base cases process atomic data. This is where the recursion stops.
- One or more recursive cases process compound data. Processing continues via recursive function calls.
type intTree =
| Empty /* atomic */
| Node(int, intTree, intTree); /* compound */
let rec summarizeTree = (t: intTree) =>
switch t {
| Empty => 0 /* base case */
| Node(i, leftTree, rightTree) => /* recursive case */
i + summarizeTree(leftTree) + summarizeTree(rightTree)
};
- Looping via higher-order helper functions
/* use recursion */
let rec summarizeList = (l: list(int)) =>
switch l {
| [] => 0
| [head, ...tail] =>
head + summarizeList(tail)
};
/* use high-order helper function */
let summarizeList = (theList: list(int)) =>
ListLabels.fold_left(
~f = (sum, elem) => sum + elem,
~init = 0,
theList
);
Other high-order helper functions to list
- map: (~f: ('a) => 'b, list('a)) => list('b) Produces a new list by applying ~f to each element of the list.
- filter: (~f: ('a) => bool, list('a)) => list('a) Produces a new list that contains only those elements of the input list for which ~f returns true.
- for_all: (~f: ('a) => bool, list('a)) => bool Returns true if ~f returns true for all elements of the list.
- exists: (~f: ('a) => bool, list('a)) => bool Returns true if ~f returns true for at least one element of the list.
- iter: (~f: ('a) => unit, list('a)) => unit Invokes ~f for each element of the list. iter() only makes sense if ~f has side effects (output, mutations, etc.).
- Recursive technique: accumulator
/* use recursion */
let rec maxList = (l: list(int)) =>
switch (l) {
| [] => min_int
| [head, ...tail] when head > maxList(tail) => head /* A */
| [_, ...tail] => maxList(tail) /* B */
};
/* use accumulator */
let rec maxList = (~max=min_int, l: list(int)) => {
switch l {
| [] => max
| [head, ...tail] when head > max =>
maxList(~max=head, tail);
| [_, ...tail] =>
maxList(~max, tail);
};
};
accumulator: a parameter that is continually updated during recursion and returned at the very end
- Tail call elimination
/* recursion without tail call*/
let rec repeat = (~times: int, str: string) =>
if (times <= 0) { /* base case */
"";
} else { /* recursive case */
str ++ repeat(~times = times - 1, str); /* A */
};
/* recursion with tail call */
let repeat = (~times: int, str: string) => {
let rec aux = (~result="", n) =>
if (n <= 0) { /* base case */
result;
} else { /* recursive case */
aux(~result = result ++ str, n-1); /* A */
};
aux(times);
};
Terminology:
- A function whose recursive calls are tail calls is called tail-recursive.
- A tail-recursive algorithm is also called iterative. Iterative algorithms can also be implemented via loops.
/* recursion without tail call */
let rec reverse = (l: list('a)) =>
switch l {
| [] => l
| [head, ...tail] =>
List.append(reverse(tail), [head]) /* A */
};
/* recursion with tail call */
let rec reverse = (theList: list('a)) => {
let rec aux = (~result=[], l) =>
switch l {
| [] => result
| [head, ...tail] =>
aux(~result=[head, ...result], tail)
};
aux(theList);
};
Difference:
- The recursive reverse() works with:
- Current data:
head - Future data:
reverse(tail)
- Current data:
- The iterative reverse() works with:
- Past data:
result - Current data:
head
- Past data: