I think this comment from benrg is a useful insight:
There is a binary operator in lambda calculus, namely function application. You could argue that it's a two-argument function.
In other words, we can express anything in the form apply(function, argument)
by carefully choosing values for function
and argument
.
In the expression 1 + 3
, we have three pieces of information: the operator/function add
, and the numerals 1
and 3
. If we had a function apply(function, argument1, argument2)
, we could write apply(add, 2, 3)
. But if we have only apply(function, argument)
, where does the extra piece of information go?
The answer is into the definition of function
: instead of add
, we define a function add2
, so that we can write apply(add2, 3)
.
Let's start by defining add1
. If you only need to handle a finite set of numbers, you can do this with an exhaustive lookup table. In Haskell, you could do this with pattern matching; in JavaScript, you could use a switch
statement:
add1 = (n) => {
switch(n) {
case 0: return 1;
case 1: return 2;
case 2: return 3;
// etc
}
}
We could use the same technique to write add2
, add3
, and so on; but actually we don't need to if we realise that adding 2 is the same as adding 1, then adding 1 again. In other words, given a definition of add1
, we can do this:
add2 = (n) => add1(add1(n));
add3 = (n) => add1(add1(add1(n)));
// etc
The only extra case we need is add0
, which just returns its input unchanged. This is known as the "identity function":
identity = (x) => x
add0 = identity
To add any pair of numbers, we need a function which takes a number, and returns a function which adds that number: for the input 2
, return the function add2
, and so on. Again, a simple implementation could use a lookup table:
add = (m) => {
switch(m) {
case 0: return add0;
case 1: return add1;
case 2: return add2;
case 3: return add3;
// ...
}
}
So now, without ever defining 2-ary addition directly, we have our curried implementation:
add(2)(3);
There's various ways we can take this further. One is by deciding the pattern of repeatedly applying add1
is something we might want to use with other functions. So we could write some helpers which take a function, and return a new function applying it multiple times:
repeat2 = (func) => (x) => func(func(x))
add2 = repeat2(add1)
As before, there are some special cases to consider:
repeat0
needs to ignore the function passed to it, and use a function that "does nothing" in its place: the identity
function we defined earlier. In other words, repeat0(anything)(n)
will reduce to identity(n)
, which is just n
.
repeat1
could be written the same way as the others, as (func) => (x) => func(x)
, but (x) => func(x)
is equivalent to func
. That leaves (func) => func
, which is just identity
again. That makes sense: repeating something once is the same as just doing the thing.
repeat0 = (func) => identity
repeat1 = identity
Then, just as we did for add
, we can write a general repeat
function which takes a number, and returns an appropriate repeatN
function:
repeat = (times) => {
switch (times) {
case 0: return repeat0;
case 1: return repeat1;
case 2: return repeat2;
case 3: return repeat3;
// ...
}
}
Or, if we skip the separate named functions, and put everything into repeat
itself:
repeat = (times) => {
switch (times) {
case 0: return (func) => identity;
case 1: return identity;
case 2: return (func) => (x) => func(func(x));
case 3: return (func) => (x) => func(func(func(x)));
// ...
}
}
Now we can simplify our definitions of add2
, add3
, etc:
add2 = repeat(2)(add1)
add3 = repeat(3)(add1)
Another thing we can do is skip the named definitions of add2
, add3
, etc and move their definitions inside add
itself:
add = (m) => {
switch(m) {
case 0: return identity;
case 1: return add1;
case 2: return repeat(2)(add1);
case 3: return repeat(3)(add1);
// ...
}
}
But hang on, we know that repeat(0)(add1)
is identity
, and repeat(1)(add1)
is add1
So we don't actually need the lookup table any more, we can just use repeat
:
add = (m) => repeat(m)(add1)
So we've defined add
based on just three functions: identity
, repeat
, and add1
.
We can carry on further if we want to: Church encoding gives definitions for both add1
(a successor function) and repeat
using recursive application of a single function, rather than a lookup table.
+
is your answer. $\endgroup$+
operator. $\endgroup$