$$
\DeclareMathOperator{\fst}{fst}
\DeclareMathOperator{\snd}{snd}
\DeclareMathOperator{\succ}{succ}
\DeclareMathOperator{\z}{zero}
\DeclareMathOperator{\fact}{fact}
$$
There is also another way to formally define such recursive functions without fixpoint operators. I think it might be interesting for you since it is probably also covered in a course for semantics of progrmming languages!
We will define the factorial function as a function acting on the inductive data type of natural numbers.
Define the natural numbers as the inductive data type $Nat$ using the following signature $\Sigma$:
data Nat = zero | succ: Nat -> Nat
For example, we represent 0 as $\z$, 1 as $\succ 0$ and 3 as $\succ (\succ (\succ \z))$. $\z$ and $\succ$ are called constructors and effectively describe all valid terms induced by this definition.
You can now define $every$ primitive recursive function on Nat by specifying an interpretation of $\z$ and $\succ$. I will explain it below in more mathematical terms, but for now think of a computer program:
// JavaScript syntax
const z = function() {
// Zero arguments because zero is a constructor taking 0 arguments
// in our signature \Sigma
return ???;
};
const succ = function(x) {
// One argument because succ is a constructor taking exactly 1 argument
// in our signature \Sigma
return ???;
};
// The above defines a primitive recursive function on Nat
// We now evaluate it on 3
const result = succ(succ(succ(z())));
For example, if you insert return 0; and return x + 1, respectively, then this will define the "identity" function from our inductive data type into 64-bit numbers baked into JavaScript. See it live at http://jsfiddle.net/v9L0x5ef.
Using return 0; and return x + 2 will define a function doubling its argument.
Exercise: Define a function multiplying the argument by 3. Do the same with 4 (in your mind). How does a function look like multiplying the argument by $y \in \mathbb{N}$?
const y = 3;
const z = function() { return 0; }
const succ = function(x) { return x + y; }
Live at http://jsfiddle.net/v9L0x5ef/1/.
Now consider the following functions defining the desired factorial function (live version):
const z = function() {
// Mathematically, this could be expressed as a simple pair
return {
// We remember at which number we currently are: we are now at 0.
prevNumber: 0,
// Our current factorial result
currentResult: 1
};
};
const succ = function(x) {
// Remember: x is the evaluation of inner succ()s and z() calls!
return {
// Remember to keep track!
prevNumber: x.prevNumber + 1,
// The factorial definition would usually be f(n) = n * f(n-1)
// and this is exactly what we are doing here
currentResult: (x.prevNumber + 1) * x.currentResult
}
};
// This would give
// {
// prevNumber: 3
// currentResult: 6
// }
const result = succ(succ(succ(z())));
const finalResult = result.currentResult;
I lied "a little bit" above. You can only define every primitive recursive function if you allow the result being served inside a pair. Here, we have the desired result under the currentResult key.
We have now uniquely (obviously) (well-)defined the factorial function by specifying a function ("interpretation") for every constructor. We could now prove the claimed property (which was your definition):
Defining $f: \mathbb{N} \to \mathbb{N}$ by $f := \snd \circ h$, we have $f(n) = n f(n-1)$ for all $n \ge 1$ and $f(0) = 0$.
I leave this to the reader.
Mathematically, we have defined a function $h$ from all valid Nat terms to pairs:
$$h(\z) := (\z, 1)\\
h(\succ n) := (\mathrm{prev} \mapsto ((\fst \mathrm{prev}) + 1, (\snd \mathrm{prev}) \cdot ((\fst \mathrm{prev}) + 1))) (h(n))\\
\\
\fact n := \snd h(n)\\
\mathrm{Alternatively: } \fact := \snd \circ h
$$
Do note that in the definition for $h(\succ n)$ we do not use $n$ at all except as $h(n)$. This ensures that the resulting morphism is actually a homomorphism in the theory behind it.
All in all, you can define many recursive functions you see in the wild this way.
Exercise: What does the inductive data type for trees with values of type $T$ at every inner node look like? Which constructors does it have?
A generic tree data type with values of type T would look like
data Tree T = leaf | node: T -> Tree -> Tree -> Tree
node receives a value, the left and the right subtree.
Exercise: Specify interpretations of the tree constructors to sum all values in a tree with values of type $\mathbb{N}$.
$leaf \mapsto = 0, node \mapsto (x, l, r) \mapsto x + l + r$
node adds the current value to the accumulated value of both subtrees.
The theory
One considers algebras $\mathfrak{M} = (\Sigma, M, \mathfrak{M}[[\z]]: M, \mathfrak{M}[[succ]]: M \to M)$. They are triples consisting of the signature $\Sigma$, a universe $M$ and interpretations of all the constructors.
Naturally one can define the so-called term algebra $[[\ldots]$$ (e.g. called $[[Nat]]$ in case of Nat) induced by every inductive data type definition:
- Choose $\Sigma$ as from the inductive data type definition
- Choose $M$ as the set of all constructible valid terms, here $M := \{\z, \succ \z, \succ (\succ \z), \succ (\succ (\succ \z)), \ldots\}$
- Interpret every term as itself, e.g. $\mathfrak{M}[[\z]] = \z$, $\mathfrak{M}[[succ]](n) = \succ n$.
The term algebra is in fact the most general one — up to isomorphism — and it turns out that we can define every primitive recursive functions on inductive data types by specifying a homomorphism from it into a target algebra over the same signature. If the target algebra is $\mathfrak{N} = (\Sigma, N, \mathfrak{N}[[\z]]: N, \mathfrak{N}[[succ]]: N \to N)$, then a homormophism $h: M \rightarrow N$ is a function which commutes with the interpretation of the target algebra:
$$h(\mathfrak{M}[[\z]]) = \mathfrak{N}[[\z]]\\
h(\mathfrak{M}[[\succ]](n)) = \mathfrak{N}[[\succ]](h(n))
$$
Concretely with $\mathfrak{M} = [[Nat]]$: $h(\succ (\succ \z)) = \mathfrak{N}[[\succ]](\mathfrak{N}[[\succ]] (h(\z)))$
Above we exactly specified the interpretations $\mathfrak{N}[[\z]]$ and $\mathfrak{N}[[\succ]]$. We also explicitly stated such a homomorphism $h$. Have a look at $h(\mathfrak{M}[[\succ]](n)) = \mathfrak{N}[[\succ]](h(n))$. You see that the result $h$ computes is our interpretation (independent of $n$!) applied to $h(n)$. Here you see why we needed to restrict ourselves to only use $h(n)$ and not $n$ alone.
Actually, that restriction is unneeded since you can rewrite the interpretations to drag the terms along they are being applied to. Let's say you have an algebra with interpretations $\mathfrak{N}$ (which unfortunately depend on $n$!), then you can construct an algebra $\mathfrak{P}$ with these interpretations to formally solve the problem:
$$
\mathfrak{P}[[\z]] = (\mathfrak{N}[[\z]], \z)\\
\mathfrak{P}[[\succ]](n) = (\mathfrak{N}[[\succ]](\fst n, \succ (\snd n), \succ (\snd n))
$$
The universe of $\mathfrak{P}$ is (possibly a subset) of $N \times M$, where $N$ is the universe of $\mathfrak{N}$ and $M$ the universe of all constructible terms. You can see that $\mathfrak{N}[[\succ]]$ can now indeed be passed the term it is applied on.