Please take a look at other posts about functional programming in JavaScript:
- Part 1 - what Functional Programming is about
- Part 2 - Functional Pipelines
- Part 3 - the Y Combinator
- Part 4 - Monads
- Part 5 - Trampoline
- Part 6 - Lenses
- Part 7 - Church encoding (booleans)
- Part 8 - Church encoding (arithmetics)
- Part 9 - Church encoding (if, recursion)
- Part 10 - Church encoding (lists)
In the next blog entry, we continue the Church encoding of various language elements into JavaScript. We already did booleans, numbers, IF and recursion.
This time we'll do lists.
Lists can be implemented using pairs, a list of (1,2,3,4) is just (1, (2, (3, 4))), nested pairs. Other complex objects can be built on top of this idea.
If you need PAIR/FIRST/SECOND, just go back to the previous blog entry.
// NIL is a pair where the first element is TRUE (a flag for "I am empty") const NIL = PAIR(TRUE)(TRUE); const IS_NIL = l => FIRST(l); // Returns TRUE if it's NIL, FALSE if it's a real PAIR // Re-defining PAIR for numbers to ensure FIRST(l) is always FALSE const LIST_NODE = x => y => PAIR(FALSE)(PAIR(x)(y)); const HEAD = l => FIRST(SECOND(l)); const TAIL = l => SECOND(SECOND(l));
And that's it! We can define a list and an example operation on a list:
// Building the list: [1, 2, 3]
const list1 = LIST_NODE(ONE)(NIL);
const list2 = LIST_NODE(TWO)(list1);
const list3 = LIST_NODE(THREE)(list2); // This is [3, 2, 1]
// --- sum function
const SUMLIST = Y(self => list =>
IF (IS_NIL(list))
(() => ZERO)
(() => ADD(HEAD(list))(self(TAIL(list)))));
console.log( toInt(SUMLIST(list3)) );
We could also have an auxiliary toArray function, similar to previous toInt and toBool:
const toArray = (list, elementConverter = (x => x)) => {
const result = [];
let current = list;
// While IS_NIL is FALSE (using toInt to bridge the Church Boolean to JS)
while (toInt(IS_NIL(current)) === 0) {
// 1. Get the data from the current node
const churchValue = HEAD(current);
// 2. Convert and push to our JS array
result.push(elementConverter(churchValue));
// 3. Move to the next node
current = TAIL(current);
}
return result;
};
console.log( toArray( list3, toInt ) ); // [3,2,1]
How about MAP or other list operations that just transform list to another list?
Well, using all the tools we have, it's easy:
const MAP = Y(self => f => list =>
IF (IS_NIL(list))
(() => NIL)
(() => LIST_NODE(f(HEAD(list)))(self(f)(TAIL(list)))));
// Example: Double every number in the list
const DOUBLE = n => ADD(n)(n);
const doubledList = MAP(DOUBLE)(list3); // [6, 4, 2]
console.log("First item doubled:", toInt(HEAD(doubledList))); // 6
console.log("Total:", toInt(SUMLIST(doubledList))); // 12
And that's it. You can continue from here on your own. Define other operators, write more high level functions. Experiment.
Just to give you some more ideas:
const LEQ = m => n => IS_ZERO(SUB(m)(n)); // no negative numbers, subtracting from ZERO stays at ZERO! const GT = m => n => NOT(LEQ(m)(n)); const LT = m => n => NOT(LEQ(n)(m)); console.log( toBool( LEQ(TWO)(FOUR)) ); // true console.log( toBool( LEQ(FOUR)(TWO)) ); // false console.log( toBool( LEQ(TWO)(TWO)) ); // true console.log( toBool( LT(TWO)(FOUR))); // true console.log( toBool( LT(FOUR)(TWO))); // false console.log( toBool( LT(FOUR)(TWO)) ); // false
A complete code of all examples of Church encoding:
const TRUE = a => b => a;
const FALSE = a => b => b;
const AND = p => q => p(q)(p);
const OR = p => q => p(p)(q);
const NOT = p => p(FALSE)(TRUE);
const toBool = f => f(true)(false);
console.log( toBool( TRUE ) );
console.log( toBool( FALSE ) );
console.log( toBool( AND(TRUE)(TRUE) ) );
console.log( toBool( AND(FALSE)(TRUE) ) );
console.log( toBool( AND(TRUE)(FALSE) ) );
console.log( toBool( AND(FALSE)(FALSE) ) );
console.log( toBool( OR(TRUE)(TRUE) ) );
console.log( toBool( OR(FALSE)(TRUE) ) );
console.log( toBool( OR(TRUE)(FALSE) ) );
console.log( toBool( OR(FALSE)(FALSE) ) );
console.log( toBool( NOT(TRUE) ) );
console.log( toBool( NOT(FALSE) ) );
const ZERO = f => x => x;
const ONE = f => x => f(x);
const TWO = f => x => f(f(x));
const THREE = f => x => f(f(f(x)));
const toInt = n => n(i => i + 1)(0);
console.log( toInt( ZERO ) );
console.log( toInt( ONE ) );
console.log( toInt( TWO ) );
console.log( toInt( THREE ) );
const SUCC = n => f => x => f(n(f)(x));
const FOUR = SUCC(THREE);
console.log( toInt( FOUR ) );
const ADD = m => n => f => x => m(f)(n(f)(x));
const MUL = m => n => f => m(n(f));
console.log( toInt( ADD(TWO)(THREE) ) );
console.log( toInt( MUL(TWO)(THREE) ) );
//const POW = m => n => n(m);
const POW = m => n => f => n(m)(f);
console.log( toInt( POW(TWO)(FOUR) ) );
// --- lists
const PAIR = x => y => f => f(x)(y);
const FIRST = p => p(TRUE);
const SECOND = p => p(FALSE);
// A helper that takes a pair (n, n+1) and returns (n+1, n+2)
const PHI = p => PAIR(SECOND(p))(SUCC(SECOND(p)));
// PRED starts at (0, 0), applies PHI 'n' times, then takes the FIRST element
const PRED = n => FIRST(n(PHI)(PAIR(ZERO)(ZERO)));
// SUBTRACT(m)(n) is just applying PRED to m, n times.
const SUB = m => n => n(PRED)(m);
console.log( toInt( PRED(FOUR) ) );
console.log( toInt( SUB(FOUR)(TWO) ) );
console.log( toInt( SUB(TWO)(FOUR) ) );
// --- if
const IF = p => a => b => p(a)(b)();
// --- recursion
const Y = f => (x => x(x))(x => f(y => x(x)(y)));
const IS_ZERO = n => n(x => FALSE)(TRUE);
const EQUALS = m => n =>
AND (IS_ZERO(SUB(m)(n)))
(IS_ZERO(SUB(n)(m)));
// --- THE FAC FUNCTION ---
const FAC = Y(self => n =>
IF (EQUALS(n)(ONE)) // if n == 1
(() => ONE) // return 1
(() => MUL(n)(self(PRED(n))))); // else return n * fac(n-1)
console.log( toInt( FAC(FOUR) ) );
// NIL is a pair where the first element is TRUE (a flag for "I am empty")
const NIL = PAIR(TRUE)(TRUE);
const IS_NIL = l => FIRST(l); // Returns TRUE if it's NIL, FALSE if it's a real PAIR
// Re-defining PAIR for numbers to ensure FIRST(l) is always FALSE
const LIST_NODE = x => y => PAIR(FALSE)(PAIR(x)(y));
const HEAD = l => FIRST(SECOND(l));
const TAIL = l => SECOND(SECOND(l));
// Building the list: [1, 2, 3]
const list1 = LIST_NODE(ONE)(NIL);
const list2 = LIST_NODE(TWO)(list1);
const list3 = LIST_NODE(THREE)(list2); // This is [3, 2, 1]
// --- sum list
const SUMLIST = Y(self => list =>
IF (IS_NIL(list))
(() => ZERO)
(() => ADD(HEAD(list))(self(TAIL(list)))));
console.log( toInt(SUMLIST(list3)) );
const toArray = (list, elementConverter = (x => x)) => {
const result = [];
let current = list;
// While IS_NIL is FALSE (using toInt to bridge the Church Boolean to JS)
while (toInt(IS_NIL(current)) === 0) {
// 1. Get the data from the current node
const churchValue = HEAD(current);
// 2. Convert and push to our JS array
result.push(elementConverter(churchValue));
// 3. Move to the next node
current = TAIL(current);
}
return result;
};
console.log( toArray( list3, toInt ) ); // [3,2,1]
// --- map
const MAP = Y(self => f => list =>
IF (IS_NIL(list))
(() => NIL)
(() => LIST_NODE(f(HEAD(list)))(self(f)(TAIL(list)))));
// Example: Double every number in the list
const DOUBLE = n => ADD(n)(n);
const doubledList = MAP(DOUBLE)(list3); // [6, 4, 2]
console.log("First item doubled:", toInt(HEAD(doubledList))); // 6
console.log("Total:", toInt(SUMLIST(doubledList))); // 6
const LEQ = m => n => IS_ZERO(SUB(m)(n)); // no negative numbers, subtracting from ZERO stays at ZERO!
const GT = m => n => NOT(LEQ(m)(n));
const LT = m => n => NOT(LEQ(n)(m));
console.log( toBool( LEQ(TWO)(FOUR)) ); // true
console.log( toBool( LEQ(FOUR)(TWO)) ); // false
console.log( toBool( LEQ(TWO)(TWO)) ); // true
console.log( toBool( LT(TWO)(FOUR))); // true
console.log( toBool( LT(FOUR)(TWO))); // false
console.log( toBool( LT(FOUR)(TWO)) ); // false
Happy coding!