Nothing but Functions - Logic
Most programmers have heard of Turing machines, an oversimplified model of computation as capable (algorithmically) as any programming language. They're useful for studing the capabilities of computers in a formal, no-frills setting. However, there exists a second, lesser-known model of computation which came slightly before Turing machines and is equally powerful: lambda calculus. (Don't panic — this isn't the kind of "calculus" found in most math classes.)
Anything that a Turing machine can do, so can lambda calculus. Instead of procedural things, like memory and instructions, lambda calculus models universal computation using nothing but functions. No variables, no numbers, no booleans, no conditionals, no loops, not even multiple-argument functions.
Like Turing machines, lambda calculus isn't immediately practical. Without any of the features found in modern languages, even normally-simple tasks can be cumbersome. However, it's a useful model for thinking about programming, and so it's still a valuable topic for modern programmers to understand. If you like, think of it as a game — what can we build using only functions?
Let's build something in JavaScript following the rules of lambda calculus. To preserve our sanity, we'll use ECMAScript 6 Arrow Functions (see MDN or the spec itself), which are available in Firefox 22 or Chrome 45. In this syntax, writing a => b
is the same as writing (function(a){ return b; })
.
Here are the rules:
- Functions can only take exactly one argument and contain exactly one statement.
- Closures are allowed — you can refer to arguments of functions in higher lexical scopes.
- Aliases can be created with
var
to help with legibility, but variables cannot be reassigned or used in a way that the original function could not, like passing it to itself.
Based on these rules, here is the most simple value, the identity function:
var IDENT = x => x;
This function returns whatever is passed to it, much like (function IDENT(x) { return x; })
. We'll use a convention of all-capitals names for things that are aliases for lambda calculus functions that are following the rules. We'll use single-letter lowercase letters for arguments to be consistent with argument naming in lambda calculus.
You can also make functions that make other functions to collect more than one argument. For example:
var RETURN_FIRST_ARG = a => b => a; var RETURN_SECOND_ARG = a => b => b; // which is like writing, for example, var RETURN_FIRST_ARG = function (a) { return function(b) { return a; }; } // then, later RETURN_FIRST_ARG("first")("second"); // "first" RETURN_SECOND_ARG("first")("second"); // "second"
These functions store an argument and then immediately return another function awaiting another argument. When you call them, you pass each argument individually as a new function call. While this seems silly, it allows lambda calculus to stay as simple as possible. It also permits powerful capabilities such as partial application, wherein you pass only one argument and wait to pass the other until later. We'll rely on that more later.
Now, we have one of the often-used capabilities found in lambda caluclus — selecting from multiple values. With this, we can build logic. Let's call the function that chooses its first argument "TRUE", and the function that chooses its second argument "FALSE".
var TRUE = x => y => x; var FALSE = x => y => y;
Now, we can write things like this:
TRUE("value_if_true")("value_if_false"); // "value_if_true" some_boolean("it's true")("it's false"); // tells us whether some_boolean is true or false
...which seems silly until you consider that we've implemented the equivalent of the ternary operator:
some_boolean ? "it's true" : "it's false"; // *also* tells us whether some_boolean is true or false
Now that we have conditional logic, we can implement other operators:
var AND = p => q => p(q)(p); var OR = p => q => p(p)(q);
Each of these functions takes two boolean values and produces a boolean value.
AND
returns q
(whatever it is) if p
is true, and p
otherwise (which must then be false). So, if p
is false, false is returned. Otherwise, q
is returned: if it's true, the result is true, and if it's false, the result is false. So, both p
and q
must be true for the result to be true.
OR
returns p
if p
is true (that is, if p
is true, true is returned), and otherwise returns q
(whatever it is). So, if p
is false, q
is returned: if it's true, the result is true, and if it's false, the result is false. So, both p
and q
must be false for the result to be false.
We can also produce NOT
:
var NOT = p => x => y => p(y)(x);
NOT
looks like it takes three arguments, but think of it like it only takes one, p
. It produces "a function which takes two things (x
and y
) and returns one of them" — a boolean value, like our TRUE
and FALSE
. The thing it returns is the result of p(y)(x)
, that is, calling p
but with the argument order swapped. So now, we have a function where if p
is true, it picks the second value, and if it's false, it picks the first value, just like if p
were the opposite of whatever it is. So, NOT
is a function that inverts the order of arguments for a boolean, turning TRUE
into FALSE
and vice-versa.
Sometimes, we really just want to use the boolean like a ternary operator, but we want to make it clear we're doing that explicitly:
var IF = p => x => y => p(x)(y);
This function takes a boolean and calls it with the given arguments, which doesn't really do much, but it lets us write code that is a little easier to read:
IF(condition) (true_value) (false_value);
We can combine some of these functions to produce another function core to most modern computers:
var NAND = p => q => NOT(AND(p)(q));
So, let's try out our new functions. First, we need a way to turn a boolean function back into something we can use in normal-land JavaScript:
function l2js_bool(bool) { return bool("TRUE")("FALSE"); }
This function is our lambda-to-javascript function for booleans. It expects a boolean and asks it to choose between string representations of its value. We can use it to print out the result of boolean computations:
output(["x","y","and","or","nand"].join("\t")+"\n"); [TRUE, FALSE].forEach(function(x){ [TRUE, FALSE].forEach(function(y){ output([ l2js_bool(x), l2js_bool(y), l2js_bool(AND(x)(y)), l2js_bool(OR(x)(y)), l2js_bool(NAND(x)(y)), ].join("\t")+"\n"); }) });
This gives us a nice truth table:
x y and or nand FALSE FALSE FALSE FALSE TRUE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE TRUE TRUE TRUE TRUE TRUE FALSE
Now, we have boolean logic using nothing but functions! In the next article, we'll build numbers and do some math. Stay tuned!