Mutual induction and recursion over natural numbers

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.

Resources

Mutual recursion in OCaml

<expression> ::= ... | let rec <name> = fun <formal> -> <expression> in <expression> | let rec <name> = fun <formal> -> <expression> and ... and <name> = fun <formal> -> <expression> in <expression>

The corresponding typing rule reads as follows, where G and G' stand for <type_environment>, f1, x1, ..., fN, and xN stand for <formal>, e0, e1, ... and eN stand for <expression>, and t0, t1, t1', ..., tN, and tN' stand for <type>:

RECG' |- fun x1 -> e1 : t1 -> t1'...G' |- fun xN -> eN : tN -> tN'G' |- e0 : t0where G' = (f1 : t1 -> t1'), ..., (fN : tN -> tN'), G
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',

  1. fun x1 -> e1 has type t1 -> t1',
  2. fun x2 -> e2 has type t2 -> t2',

...

  1. fun xN -> eN has type tN -> tN', and
  1. e0 has type t0.

Note: f1, ..., and fN must be distinct from each other.

Furthermore,

  • to evaluate let rec f1 = fun x1 -> e1 and ... and fN = fun xN -> eN in e0,

    • we first evaluate the definienses, i.e., fun x1 -> e1, ..., and fun xN -> eN, in an extension of the current environment where f1 denotes the result of evaluating fun x1 -> e1, ..., and fN denotes the result of evaluating fun xN -> eN in this extended environment; it is this self-reference that enables the recursive definitions of f1, ..., and fN;

    • evaluating let rec f1 = fun x1 -> e1 and ... and fN = fun xN -> eN in e0 then reduces to evaluating e0 in this extended environment;

      the binding of f1, ..., and fN are local: they vanish once e0 is evaluated.

Even or odd

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)
      (* a random instance of the induction step: *)
  and is = (let i' = Random.int 1000
            in candidate (succ i') = not (candidate i'))
  (* 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)
      (* a random 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:

  • base case (i = 0): 0 is even and 0 is not odd
  • induction step (i = succ i’): given a number i' such that its evenness is the Boolean ih_even and that its oddness is the Boolean ih_odd, the evenness of succ i' is the oddness of i', i.e., ih_odd, and the oddness of succ i' is the evenness of i', i.e., ih_even

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)
      (* random instances of the induction steps: *)
  and b2 = (let i' = Random.int 1000
            in evenp (succ i') = oddp i')
  and b3 = (let i' = Random.int 1000
            in oddp (succ i') = evenp i')
  (* 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 i =
  if i = 0
  then true
  else let i' = pred i
       in oddp_v3 i'
and oddp_v3 i =
  if i = 0
  then false
  else let i' = pred i
       in evenp_v3 i';;

The two functions evenp_v3 and oddp_v3 call each other. They are mutually recursive, i.e., indirectly self-referential, like the last sentences of Exercise 21 – self-referential sentences and like Exercise 27 – multiple-choice questions in Week 01, 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 (n >= 0) in
  evenp_v3 n;;

let oddp_v4 n =
  let () = assert (n >= 0) 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 indentation =
  let () = Printf.printf "%sevenp_v3 %d ->\n" (indent indentation) n in
  if n = 0
  then true
  else let n' = pred n
       in let result = traced_oddp_v3 n' (indentation + 1)
          in let () = Printf.printf "%sevenp_v3 %d <- %b\n" (indent indentation) n result in
             result
and traced_oddp_v3 n indentation =
  let () = Printf.printf "%sodd_v3 %d ->\n" (indent indentation) n in
  if n = 0
  then false
  else let n' = pred n
       in let result = traced_evenp_v3 n' (indentation + 1)
          in let () = Printf.printf "%soddp_v3 %d <- %b\n" (indent indentation) n result in
             result;;

let traced_evenp_v4 n =
  let () = Printf.printf "evenp_v4 %d ->\n" n in
  let () = assert (n >= 0) 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 (n >= 0) in
  let result = traced_oddp_v3 n 1
  in let () = Printf.printf "oddp_v4 %d <- %b\n" n result in
     result;;

Specifically,

  • when traced_evenp_v3 is called on a number n, it emits the trace evenp_v3 n ->,
  • when traced_evenp_v3 returns a result b, it emits the trace evenp_v3 n <- b,
  • when traced_oddp_v3 is called on a number n, it emits the trace oddp_v3 n ->,
  • when traced_oddp_v3 returns a result b, it emits the trace oddp_v3 n <- b,
  • when traced_evenp_v4 is called on a number n, it emits the trace evenp_v4 n ->,
  • when traced_evenp_v4 returns a result b, it emits the trace evenp_v4 n <- b,
  • when traced_oddp_v4 is called on a number n, it emits the trace oddp_v4 n ->, and
  • when traced_oddp_v4 returns a result b, it emits the trace oddp_v4 n <- b.

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
#

A closer look at the definitions of the even and odd predicates

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' = pred n
       in oddp_v3 n'
and oddp_v3 n =
  if n = 0
  then false
  else let n' = pred n
       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.

Pablito (popping in): Hi guys! Where is the reward?
Anton: You are coming from the index, aren’t you?
Alfrothul: ‘fraid you’re on our own here.
Dana: What a lot of owls.

A more faithful trace for the even and odd predicates

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' = pred n
       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' = pred n
       in traced_evenp_v5 n';;

let traced_evenp_v6 n =
  let () = Printf.printf "evenp_v6 %d ->\n" n in
  let () = assert (n >= 0) 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 (n >= 0) 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:

  • recursive functions with n calls elicit n returns, and
  • tail-recursive functions with n (tail) calls elicit 1 return.

Exercise 30

Generalizing from 2 to 3, a non-negative integer 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. The goal of this exercise is to implement 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. To this end:

  1. Create a unit-test function for the three predicates.
  2. Implement each of the three predicates independently as single recursive functions.
  3. Implement the three predicates as mutually recursive functions.

If you feel inspired to trace your implementations, do so as in the section about A more faithful trace for the even and odd predicates, just above.

Declaring global mutually recursive functions

OCaml provides language support to declare global mutually recursive functions:

<toplevel-expression> ::= <expression> | let <formal> = <definiens> | let rec <name> = fun <formal> -> <expression> | let rec <name> = fun <formal> -> <expression> and ... and <name> = fun <formal> -> <expression>

Resources

Version

Stated that the instances of the induction step is random in test_evenp, test_oddp, and test_evenp_and_oddp [04 Mar 2023]

Quantified the variables in the type-inference rules [23 Jan 2023]

Created [16 Sep 2022]