You can write them using continuations -
const cons = (a,b) => k => k(a,b)
const car = k => k((a,b) => a)
const cdr = k => k((a,b) => b)
const l = cons(1, cons(2, cons(3, null)))
console.log(car(l), car(cdr(l)), car(cdr(cdr(l))), cdr(cdr(cdr(l))))
// 1 2 3 null
Define more like list
, toString
, map
, and filter
-
const cons = (a,b) => k => k(a,b)
const car = k => k((a,b) => a)
const cdr = k => k((a,b) => b)
const list = (v, ...vs) => v == null ? null : cons(v, list(...vs))
const toString = l => l == null ? "Ø" : car(l) + "->" + toString(cdr(l))
console.log(toString(list(1,2,3,4,5)))
// 1->2->3->4->5->Ø
const square = x => x * x
const map = (f, l) =>
l == null
? null
: cons(f(car(l)), map(f, cdr(l)))
console.log(toString(map(square, list(1,2,3,4,5))))
// 1->4->9->16->25->Ø
const isOdd = x => x & 1
const filter = (f, l) =>
l == null
? null
: Boolean(f(car(l)))
? cons(car(l), filter(f, cdr(l)))
: filter(f, cdr(l))
console.log(toString(filter(isOdd, map(square, list(1,2,3,4,5)))))
// 1->9->25->Ø
Note you could just as easily write cons
, car
, and cdr
abstractions using an array. Lambda is more fun but anything that fulfills the contract is acceptable. Being able to change the underlying representation like this and not require changes in other parts of your program is what makes data abstraction a powerful technique.
const cons = (a,b) => [a,b]
const car = k => k[0]
const cdr = k => k[1]
const l = cons(1, cons(2, cons(3, null)))
console.log(car(l), car(cdr(l)), car(cdr(cdr(l))), cdr(cdr(cdr(l))))
// 1 2 3 null