Functions (ECMAScript)
All you need are function definitions and function calls. You don't need any branching, conditionals, operators or builtin functions. I will demonstrate an implementation using ECMAScript.
First, let's define two functions called true
and false
. We could define them any way we want, they are completely arbitrary, but we will define them in a very special way which has some advantages as we will see later:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
tru
is a function with two parameters which simply ignores its second argument and returns the first. fls
is also a function with two parameters which simply ignores its first argument and returns the second.
Why did we encode tru
and fls
this way? Well, this way, the two functions not only represent the two concepts of true
and false
, no, at the same time, they also represent the concept of "choice", in other words, they are also an if
/then
/else
expression! We evaluate the if
condition and pass it the then
block and the else
block as arguments. If the condition evaluates to tru
, it will return the then
block, if it evaluates to fls
, it will return the else
block. Here's an example:
tru(23, 42);
// => 23
This returns 23
, and this:
fls(23, 42);
// => 42
returns 42
, just as you would expect.
There is a wrinkle, however:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
This prints both then branch
and else branch
! Why?
Well, it returns the return value of the first argument, but it evaluates both arguments, since ECMAScript is strict and always evaluates all arguments to a function before calling the function. IOW: it evaluates the first argument which is console.log("then branch")
, which simply returns undefined
and has the side-effect of printing then branch
to the console, and it evaluates the second argument, which also returns undefined
and prints to the console as a side-effect. Then, it returns the first undefined
.
In λ-calculus, where this encoding was invented, that's not a problem: λ-calculus is pure, which means it doesn't have any side-effects; therefore you would never notice that the second argument also gets evaluated. Plus, λ-calculus is lazy (or at least, it is often evaluated under normal order), meaning, it doesn't actually evaluate arguments which aren't needed. So, IOW: in λ-calculus the second argument would never be evaluated, and if it were, we wouldn't notice.
ECMAScript, however, is strict, i.e. it always evaluates all arguments. Well, actually, not always: the if
/then
/else
, for example, only evaluates the then
branch if the condition is true
and only evaluates the else
branch if the condition is false
. And we want to replicate this behavior with our iff
. Thankfully, even though ECMAScript isn't lazy, it has a way to delay the evaluation of a piece of code, the same way almost every other language does: wrap it in a function, and if you never call that function, the code will never get executed.
So, we wrap both blocks in a function, and at the end call the function that is returned:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
prints then branch
and
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
prints else branch
.
We could implement the traditional if
/then
/else
this way:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Again, we need some extra function wrapping when calling the iff
function and the extra function call parentheses in the definition of iff
, for the same reason as above:
const iff = (cnd, thn, els) => cnd(thn, els)();
iff(tru, () => console.log("then branch"), () => console.log("else branch"));
// then branch
iff(fls, () => console.log("then branch"), () => console.log("else branch"));
// else branch
Now that we have those two definitions, we can implement or
. First, we look at the truth table for or
: if the first operand is truthy, then the result of the expression is the same as the first operand. Otherwise, the result of the expression is the result of the second operand. In short: if the first operand is true
, we return the first operand, otherwise we return the second operand:
const orr = (a, b) => iff(a, () => a, () => b);
Let's check out that it works:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Great! However, that definition looks a little ugly. Remember, tru
and fls
already act like a conditional all by themselves, so really there is no need for iff
, and thus all of that function wrapping at all:
const orr = (a, b) => a(a, b);
There you have it: or
(plus other boolean operators) defined with nothing but function definitions and function calls in just a handful of lines:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els,
orr = (a , b ) => a(a, b),
nnd = (a , b ) => a(b, a),
ntt = a => a(fls, tru),
xor = (a , b ) => a(ntt(b), b),
iff = (cnd, thn, els) => cnd(thn, els)();
Unfortunately, this implementation is rather useless: there are no functions or operators in ECMAScript which return tru
or fls
, they all return true
or false
, so we can't use them with our functions. But there's still a lot we can do. For example, this is an implementation of a singly-linked list:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Objects (Scala)
You may have noticed something peculiar: tru
and fls
play a double role, they act both as the data values true
and false
, but at the same time, they also act as a conditional expression. They are data and behavior, bundled up into one … uhm … "thing" … or (dare I say) object!
Indeed, tru
and fls
are objects. And, if you have ever used Smalltalk, Self, Newspeak or other object-oriented languages, you will have noticed that they implement booleans in exactly the same way. I will demonstrate such an implementation here in Scala:
sealed abstract trait Buul {
def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): T
def &&&(other: ⇒ Buul): Buul
def |||(other: ⇒ Buul): Buul
def ntt: Buul
}
case object Tru extends Buul {
override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): U = thn
override def &&&(other: ⇒ Buul) = other
override def |||(other: ⇒ Buul): this.type = this
override def ntt = Fls
}
case object Fls extends Buul {
override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): V = els
override def &&&(other: ⇒ Buul): this.type = this
override def |||(other: ⇒ Buul) = other
override def ntt = Tru
}
object BuulExtension {
import scala.language.implicitConversions
implicit def boolean2Buul(b: ⇒ Boolean) = if (b) Tru else Fls
}
import BuulExtension._
(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3
This BTW is why the Replace Conditional With Polymorphism Refactoring always works: you can always replace any and every conditional in your program with polymorphic message dispatch, because as we have just shown, polymorphic message dispatch can replace conditionals by simply implementing them. Languages like Smalltalk, Self and Newspeak are existence proof of that, because those languages don't even have conditionals. (They also don't have loops, BTW, or really any sort of language built-in control structures except for polymorphic message dispatch aka virtual method calls.)
Pattern Matching (Haskell)
You could also define or
using pattern matching, or something like Haskell's partial function definitions:
True ||| _ = True
_ ||| b = b
Of course, pattern matching is a form of conditional execution, but then again, so is object-oriented message dispatch.
return arg1 ? arg1 : arg2;
or
operator.