Church encoding in JavaScript

Weak typing in JavaScript makes lambda calculus’ church encoding a cinch
2022-09-28

In the office recently we were chatting about encoding natural numbers in functional programming. One of my colleagues isn’t a Programming Languages theory person, and wanted to build a bit of intuition for it – we asked him how he’d implement addition if he could not use the + operator in JavaScript. This is, predictably, a bit challenging – the base number type in JS isn’t structural like it might be in a language like OCaml, it’s all floats all the way down.

Natural numbers in OCaml

We demonstrated what an encoding of natural numbers would look like in OCaml:

type nat = Z | S of nat
let two = S (S Z)
let three = S (S (S Z))

With this definition we can think about what some operators on naturals would be. The obvious starting point are the successor (succ) and predecessor (pred) functions.

let succ n = S n
let pred = function 
  S n -> n
| Z -> Z

Simply, succ adds a S constructor to our number; similarly pred pops a successor off a number, and it bottoms out at zero.

Okay, so what about addition?

let rec add a = function
  S n -> add (S a) n
| Z -> a

add two three
(* - : nat = S (S (S (S (S Z)))) *)

This function pops all the successors off the 2nd argument and puts them on the front of the first argument, until there’s none left. Addition!

Church numerals in Javascript

This doesn’t help us in Javascript though. Our encoding of naturals here is structural in the types available in the language, we could probably fake that up in Javascript using lists or objects, but a far more interesting way to do this is something called Church Encoding.

A church numeral is a number which is represented as a higher-order function. A church numeral takes 2 arguments, a function f and a base-case x – the function f is applied n times for the value n encoded as a church numeral.

(* Apply f zero times to x *)
let zero _f x = x
(* Apply f three times to x *)
let three f x = f (f (f x))

This can be written in Javascript more conveniently!

const zero = f => x => x
const three = f => x => f(f(f(x)))

What does it look like to use a church numeral in JavaScript? Well, it applies a function n times to x, so we can use it to count:

/* Count to 3 (starting at 1) */
const count_one = x => { console.log(x); return x + 1 }
three(count_one)(1)
// 1
// 2
// 3

Similarly, we can convert these Church numerals to normal JavaScript numbers:

console.log( three(x => x+1)(0) )
// 3

And we can implement a general function for casting from Church numerals to JavaScript numbers.

const ch_to_num = n => n(x => x+1)(0)

We don’t yet have a way to build these numbers though – we need to implement a successor function.

const succ = n => f => x => f ( n(f)(x) )

The part inside the brackets n(f)(x) applies f n times to x, and the extra f outside the brackets applies it one more time. Successor!

Now we can implement a function which converts normal JavaScript numbers to church numerals too.

const num_to_ch = n => { 
  if (n == 0) { 
    return zero 
  } else { 
    return succ (num_to_ch(n-1))
  }
}

Great, that’s convenient We haven’t looked at addition yet, though. Two church numerals m and n will apply some function f n and m times respectively. After addition of m and n we need to apply some function f m+n times.

//  Apply f to x n times, and apply f to the result m timnes.
const add = m => n => f => x => m(f)(n (f)(x))
let five = add(two)(three)
console.log(ch_to_num(five))
// 5

More fun with Church encoding

We can define a few more numeric functions in Church encoding:

const mult = m => n => f => x => n(m (f))(x)
const exp = m => n => n (m)

console.log(ch_to_num(mult(three)(two)))
// 6
console.log(ch_to_num(exp(three)(two)))
// 9

We can grab more Church encoding from PL literature, too: here is an encoding of boolean values inl and inr.

const inl = x => y => x /* stand-in for true */
const inr = x => y => y /* stand-in for false */

We can evaluate them by applying them with true for left branches, and false for right branches:

console.log(inl(true)(false))
// true
console.log(inr(true)(false))
// false

Now we can build some predicate functions, like is_zero: where for some number n it is evaluated with a base-case of true (inl), and an inductive case which maps any input to false (inr).

const is_zero = n => n(x => inr)(inl)
console.log(is_zero(zero)(true)(false))
// true
console.log(is_zero(one)(true)(false))
// false
console.log(is_zero(two)(true)(false))
// false

With booleans we can also define some logical operators:

const and = a => b => a(b)(inr)
console.log(and(inl)(inl)(true)(false))
// true
console.log(and(inr)(inl)(true)(false))
// false
console.log(and(inl)(inr)(true)(false))
// false
console.log(and(inr)(inr)(true)(false))
// false

const or = a => b => a(inl)(b)
console.log(or(inl)(inl)(true)(false))
// true
console.log(or(inr)(inl)(true)(false))
// true
console.log(or(inl)(inr)(true)(false))
// true
console.log(or(inr)(inr)(true)(false))
// false

Loops and recursion

This is where the story seems to get a little sticky. Curry found the fixed-point operator in simply typed lambda calculus called the Y-combinator.

It is defined as Y = \f (\x f x x)(\x f x x), and we can write this in the same style of JavaScript we’ve been looking at so far:

const y = f => (x => f(x(x)))(x => f(x(x)))

But when we true to evaluate this, even with a function which isn’t recursive we get unstuck.

y (f => x => x)

We get an error from NodeJS:

const y = f => (x => f(x(x)))(x => f(x(x)))
                                   ^

RangeError: Maximum call stack size exceeded

Annoyingly, the V8 JavaScript engine (which NodeJS is built on) does not support tail-call optimisation1. There is a fixed-point combinator whose definition works in an eager language like JavaScript.

const z = f => (x => f (v => x(x)(v)))(x => f (v => x(x)(v)))

This is the point where I found V8 doesn’t work for me, and WebKit is too slow. I’ll keep playing and try and build some working interesting recursion examples.


  1. A thread on stackoverflow has an on going discussion about support (or lack-thereof) for tail-call optimisation in JavaScript. The thread↩︎