The goal of this lecture note is to articulate the mutually inductive specification of a computation over natural numbers and how to program the corresponding mutually recursive functions in OCaml.
The corresponding typing rule reads as follows:
REC | (f1 : t1 -> t1'), ..., (fN : tN -> tN'), G |- fun x1 -> e1 : t1 -> t1' | ... | (f1 : t1 -> t1'), ..., (fN : tN -> tN'), G |- fun xN -> eN : tN -> tN' | (f1 : t1 -> t1'), ..., (fN : tN -> tN'), G |- e0 : t0 | |
G |- let rec f1 = fun x1 -> e1 and ... and fN = fun xN -> eN in e0 : t0 |
In words: in any type environment G, the expression let rec f1 = fun x1 -> e1 and ... and fN = fun xN -> eN in e0 has type t0 whenever, in an extension of this environment where f1 has type t1 -> t1', ..., fN has type tN -> tN',
...
Let us revisit the even and odd predicates:
let test_evenp candidate =
(* a few handpicked numbers: *)
let b1 = (candidate 1 = false)
and b2 = (candidate 2 = true)
and b3 = (candidate 3 = false)
and b4 = (candidate 4 = true)
and b5 = (candidate 5 = false)
and b6 = (candidate 1000 = true)
and b7 = (candidate 1001 = false)
(* testing the completeness of the even predicate: *)
and b8 = (let n = Random.int 1000
in candidate (2 * n) = true)
and b9 = (let n = Random.int 1000
in candidate (2 * n + 1) = false)
(* an instance of the base case: *)
and bc = (candidate 0 = true)
(* an instance of the induction step: *)
and is = (let n' = Random.int 1000
in candidate (succ n') = not (candidate n'))
(* etc. *)
in b1 && b2 && b3 && b4 && b5 && b6 && b7 && b8 && b9 && bc && is;;
let test_oddp candidate =
(* a few handpicked numbers: *)
let b1 = (candidate 1 = true)
and b2 = (candidate 2 = false)
and b3 = (candidate 3 = true)
and b4 = (candidate 4 = false)
and b5 = (candidate 5 = true)
and b6 = (candidate 1000 = false)
and b7 = (candidate 1001 = true)
(* testing the completeness of the odd predicate: *)
and b8 = (let n = Random.int 1000
in candidate (2 * n) = false)
and b9 = (let n = Random.int 1000
in candidate (2 * n + 1) = true)
(* an instance of the base case: *)
and bc = (candidate 0 = false)
(* an instance of the induction step: *)
and is = (let n' = Random.int 1000
in candidate (succ n') = not (candidate n'))
(* etc. *)
in b1 && b2 && b3 && b4 && b5 && b6 && b7 && b8 && b9 && bc && is;;
This time, however, let us define them together, since if a number is odd, its successor is even, and if a number is even, its succcessor is odd:
Since the two predicates are defined together, let us test them together:
let test_evenp_and_oddp evenp oddp =
(* an instance of the base cases: *)
let b0 = (evenp 0 = true)
and b1 = (oddp 0 = false)
(* an instance of the induction steps: *)
and b2 = (let n' = Random.int 1000
in evenp (succ n') = oddp n')
and b3 = (let n' = Random.int 1000
in oddp (succ n') = evenp n')
(* etc. *)
in b0 && b1 && b2 && b3;;
And since the two predicates are defined together, let us implement them together, i.e., in a mutually recursive way:
let rec evenp_v3 n =
if n = 0
then true
else let n' = n - 1
in oddp_v3 n'
and oddp_v3 n =
if n = 0
then false
else let n' = n - 1
in evenp_v3 n';;
The two functions evenp_v3 and oddp_v3 call each other. They are mutually recursive, and they pass the unit tests:
# test_evenp evenp_v3;;
- : bool = true
# test_oddp oddp_v3;;
- : bool = true
# test_evenp_and_oddp evenp_v3 oddp_v3;;
- : bool = true
#
To shield them against negative integers, let us provide an interface:
let evenp_v4 n =
let () = assert (0 <= n)
in evenp_v3 n;;
let oddp_v4 n =
let () = assert (0 <= n)
in oddp_v3 n;;
To visualize the computation, let us instrument these implementations with tracing information. The idea is that every call to evenp_v3 and to oddp_v3 emits a trace, and that so does every return:
let rec traced_evenp_v3 n i =
let () = Printf.printf "%sevenp_v3 %d ->\n" (indent i) n in
if n = 0
then true
else let n' = n - 1
in let result = traced_oddp_v3 n' (i + 1)
in let () = Printf.printf "%sevenp_v3 %d <- %b\n" (indent i) n result
in result
and traced_oddp_v3 n i =
let () = Printf.printf "%sodd_v3 %d ->\n" (indent i) n in
if n = 0
then false
else let n' = n - 1
in let result = traced_evenp_v3 n' (i + 1)
in let () = Printf.printf "%soddp_v3 %d <- %b\n" (indent i) n result
in result;;
let traced_evenp_v4 n =
let () = Printf.printf "evenp_v4 %d ->\n" n in
let () = assert (0 <= n)
in let result = traced_evenp_v3 n 1
in let () = Printf.printf "evenp_v4 %d <- %b\n" n result
in result;;
let traced_oddp_v4 n =
let () = Printf.printf "oddp_v4 %d ->\n" n in
let () = assert (0 <= n)
in let result = traced_oddp_v3 n 1
in let () = Printf.printf "oddp_v4 %d <- %b\n" n result
in result;;
Specifically,
In addition, each call is indented proportionally to its nesting.
Behold the trace of the even predicate (and of the odd predicate too):
# traced_evenp_v4 0;;
evenp_v4 0 ->
evenp_v3 0 ->
evenp_v4 0 <- true
- : bool = true
# traced_evenp_v4 1;;
evenp_v4 1 ->
evenp_v3 1 ->
odd_v3 0 ->
evenp_v3 1 <- false
evenp_v4 1 <- false
- : bool = false
# traced_evenp_v4 2;;
evenp_v4 2 ->
evenp_v3 2 ->
odd_v3 1 ->
evenp_v3 0 ->
oddp_v3 1 <- true
evenp_v3 2 <- true
evenp_v4 2 <- true
- : bool = true
# traced_evenp_v4 3;;
evenp_v4 3 ->
evenp_v3 3 ->
odd_v3 2 ->
evenp_v3 1 ->
odd_v3 0 ->
evenp_v3 1 <- false
oddp_v3 2 <- false
evenp_v3 3 <- false
evenp_v4 3 <- false
- : bool = false
#
And behold the trace of the odd predicate (and of the even predicate too):
# traced_oddp_v4 0;;
oddp_v4 0 ->
odd_v3 0 ->
oddp_v4 0 <- false
- : bool = false
# traced_oddp_v4 1;;
oddp_v4 1 ->
odd_v3 1 ->
evenp_v3 0 ->
oddp_v3 1 <- true
oddp_v4 1 <- true
- : bool = true
# traced_oddp_v4 2;;
oddp_v4 2 ->
odd_v3 2 ->
evenp_v3 1 ->
odd_v3 0 ->
evenp_v3 1 <- false
oddp_v3 2 <- false
oddp_v4 2 <- false
- : bool = false
# traced_oddp_v4 3;;
oddp_v4 3 ->
odd_v3 3 ->
evenp_v3 2 ->
odd_v3 1 ->
evenp_v3 0 ->
oddp_v3 1 <- true
evenp_v3 2 <- true
oddp_v3 3 <- true
oddp_v4 3 <- true
- : bool = true
#
Let us look again at the definitions of evenp_v3 and oddp_v3:
let rec evenp_v3 n =
if n = 0
then true
else let n' = n - 1
in oddp_v3 n'
and oddp_v3 n =
if n = 0
then false
else let n' = n - 1
in evenp_v3 n';;
The last thing evenp_v3 does in its alternative branch is to call oddp_v3, and the last thing oddp_v3 does in its alternative branch is to call evenp_v3. Such calls are said to be tail calls, and recursive functions where all calls are tail calls are said to be tail recursive.
Since evenp_v3 and oddp_v3 are tail recursive, let us adjust their trace to not indent and to only trace the final result:
let rec traced_evenp_v5 n =
let () = Printf.printf "evenp_v5 %d ->\n" n in
if n = 0
then true
else let n' = n - 1
in traced_oddp_v5 n'
and traced_oddp_v5 n =
let () = Printf.printf "oddp_v5 %d ->\n" n in
if n = 0
then false
else let n' = n - 1
in traced_evenp_v5 n';;
let traced_evenp_v6 n =
let () = Printf.printf "evenp_v6 %d ->\n" n in
let () = assert (0 <= n)
in let result = traced_evenp_v5 n
in let () = Printf.printf "evenp_v6 %d <- %b\n" n result
in result;;
let traced_oddp_v6 n =
let () = Printf.printf "oddp_v6 %d ->\n" n in
let () = assert (0 <= n)
in let result = traced_oddp_v5 n
in let () = Printf.printf "oddp_v6 %d <- %b\n" n result
in result;;
Behold the flat trace of the even predicate (and of the odd predicate too):
# traced_evenp_v6 5;;
evenp_v6 5 ->
evenp_v5 5 ->
oddp_v5 4 ->
evenp_v5 3 ->
oddp_v5 2 ->
evenp_v5 1 ->
oddp_v5 0 ->
evenp_v6 5 <- false
- : bool = false
#
And behold the flat trace of the odd predicate (and of the even predicate too):
# traced_oddp_v6 5;;
oddp_v6 5 ->
oddp_v5 5 ->
evenp_v5 4 ->
oddp_v5 3 ->
evenp_v5 2 ->
oddp_v5 1 ->
evenp_v5 0 ->
oddp_v6 5 <- true
- : bool = true
#
These traces illustrate how in a functional language, iteration (i.e., looping) is provided by tail recursion: all the tail calls share one single return, namely that of the first call. In a nutshell:
Generalizing from 2 to 3, a number divisible by 3 is said to be ternary, the successor of a ternary number is said to be post-ternary, and the predecessor of a positive ternary number is said to be pre-ternary. Write predicates about whether a given non-negative integer is ternary, pre-ternary, or post-ternary, in the same fashion as for the predicates about a given non-negative integer being even or odd.
OCaml provides language support to declare global mutually recursive functions:
Fine-tuned the narrative about recursive functions (n calls and n returns) vs. tail-recursive functions (n tail calls and 1 return), witness their traces [05 Apr 2020]
Updated the resource file to solely use the unparsing functions in the traces [29 Feb 2020]
Created [20 Feb 2020]