Comments:"List Out of Lambda / Steve Losh"
URL:http://stevelosh.com/blog/2013/03/list-out-of-lambda
If you ignore the practical issues of computers like size, weight, cost, heat, and so on, what do you really need in a programming language? Let’s play around with this question.
To understand this post you’ll need a basic understanding of how functions in Javascript work. If you can look at this code and understand what it prints, you’re good to go:
varx=10;varf=function(y){console.log(x);console.log(y);}varg=f;f(1);g(2);
This blog post is a thought exercise. It’s not something you’d ever use for real code. But just like a guitarist practices scales that she won’t ever play in a song, we programmers should be exercising our brains every so often.
I’m going to use Javascript for the examples. Any language with first class functions and lexical scoping (basically: closures) will work. The examples would be prettier in a Lisp, but some people would be turned off by the syntax and miss out on some interesting ideas. Feel free to port the code if it bothers you.
If you’ve already seen this kind of thing before (maybe you’ve gone through The Little Schemer or SICP) you may want to just skim the code here and look for anything new.
If you haven’t seen anything like this, then you’re in for a treat! It’s all going to look extremely weird the first time you see it. Go slowly and make sure you understand each piece fully before moving on to the next. These concepts may be unintuitive, but they’re built from very simple pieces.
Finally: if you get stuck, don’t worry. Tracing out the execution of a function on paper can be a good way to wrap your brain around it (I recommend investing in a good lap desk for comfy reading and writing). If that doesn’t work, just close the window and come back tomorrow. Sometimes new concepts need a while to rattle around in your brain before they click into place.
Lists
Let’s get started. One of the most common things we do as programmers is grouping data together. Javascript has “arrays” built in to the language for this:
varnames=["Alice","Bob","Candice"];
What if Javascript didn’t come with arrays included? Could we create them (or something like them) ourselves?
To answer this, let’s think about the bare minimum we’d need to “bootstrap” something like an array. There are a number of ways to do this, but we’re going to look at one in particular.
We’ll call our array-like thing a “list”. To make it work, we need four parts:
- The concept of “the empty list”.
- A way to add an element to the front of a list.
- A way to take a list and get the first element.
- A way to take a list and get everything but the first element.
If we have those four things, we can build on top of them to do anything else we might want. For example: to make a list of one item, you add that item to the front of the empty list.
Let’s narrow this down further. There are lots of ways you could implement those four things — I’m going to use functions. Let’s sketch out an outline:
varempty_list=null;varprepend=function(el,list){// ...};varhead=function(list){// ...};vartail=function(list){// ...};varis_empty=function(list){// ...};
Here are the descriptions of each of these items.
The empty_list
is a special value that represents a list of zero elements.
It can be anything, so for now we’ll use null
(we’ll revisit this later).
prepend(1, some_list)
will return a new list that looks like the old one, but
with 1
stuck onto the front of it. So if we want to create a list of the
numbers 1
and 2
we can say prepend(1, prepend(2, empty_list))
or “prepend
one to the result of prepending 2 to the empty list”.
head(some_list)
will return the first element in the list. Calling it on the
empty list will be undefined, so we’ll just be very careful not to do that!
tail(some_list)
will return a new list that’s like the one we gave it, but
with the first element removed. Again, calling this on an empty list will make things explode.
is_empty(some_list)
will return true
if the list given to it is the empty
list, and false
otherwise.
Once we have those four functions (plus the special empty list value) we can start building on top of them, so let’s figure out how to make them!
List Out of If
If you haven’t seen anything like this before, you might think it’s time to start creating Javascript Objects. That’s certainly one way to do it.
Since this post is a thought experiment in what we actually need, though, let’s try to avoid using big language features (like Objects) unless we absolutely can’t avoid it.
So if we don’t want to use other language features yet, what are we left with?
Well so far our skeleton only has functions (and null
), so let’s try those!
Here’s the first working revision of the building blocks of lists:
varempty_list=null;varprepend=function(el,list){returnfunction(command){if(command==="head"){returnel;}elseif(command==="tail"){returnlist;}}};varhead=function(list){returnlist("head");};vartail=function(list){returnlist("tail");};varis_empty=function(list){returnlist===null;};
Go ahead and paste that into a browser console and play with it:
vare=empty_list;console.log(is_empty(e));// truevarnames=prepend("Alice",prepend("Bob",prepend("Candice",empty_list)));console.log(is_empty(names));// Falseconsole.log(head(names));// Aliceconsole.log(tail(names));// Some function representing the list of ("Bob", "Candice")console.log(head(tail(names)));// Bob
But Where is the Data?
Did the definitions of those functions surprise you? Lists seem like such an important, object-oriented concept, but there only appear to be functions here!
Let’s look at how this actually works. First of all, the “empty list” concept is pretty straightforward:
varempty_list=null;varis_empty=function(list){returnlist===null;};
We could have picked any arbitrary value here. null
seemed appropriate, so
I used that.
Now on to the meat of things: prepend
.
varprepend=function(el,list){returnfunction(command){if(command==="head"){returnel;}elseif(command==="tail"){returnlist;}}};
This is where the real magic happens. Let’s think through it.
First of all, we know that when you prepend something to a list, you’re going to
get a (new) list back. So whatever prepend
returns must be a list.
Looking at the code, we can see it returns a function. So in our little thought experiment, a list is actually a Javascript function under the hood!
So what do we need to do with lists (aside from empty checking, which we’ve
already covered)? Well, we need to be able to get the head and the tail. When
we call prepend(h, t)
, we happen to be conveniently specifying the head and
tail as the arguments! So in prepend
we return a function that knows how to
return its own head or tail when asked.
So a “list” is “a function that knows how to return its own head or tail when
asked”. So our head
and tail
functions just need to ask nicely!
varhead=function(list){returnlist("head");};vartail=function(list){returnlist("tail");};
That’s it! We’ve created lists in 23 lines of code without using any fancy things like Objects. Before you move on, make sure you really understand why this works. Write out a few examples on paper.
varempty_list=null;varprepend=function(el,list){returnfunction(command){if(command==="head"){returnel;}elseif(command==="tail"){returnlist;}}};varhead=function(list){returnlist("head");};vartail=function(list){returnlist("tail");};varis_empty=function(list){returnlist===null;};
Building on the Foundations
Now that we have lists, let’s implement a few common things on top of them as practice.
map
A common thing to do to a list is to create a new list by looping through it and doing something to each item. This is called “map”:
varmap=function(fn,l){if(is_empty(l)){returnempty_list;}else{returnprepend(fn(head(l)),map(fn,tail(l)));}};
If you’re not used to recursive definitions like this, you may way to take a few minutes and try to work out how it works. Here’s an example:
varsquare=function(x){returnx*x;}varnumbers=prepend(1,prepend(2,prepend(3,empty_list)));varsquared_numbers=map(square,numbers);// map(square, [1, 2, 3])// prepend(square(1), map(square, [1, 2, 3]))// prepend(square(1), prepend(square(2), map(square, [3])))// prepend(square(1), prepend(square(2), prepend(square(3), map(square, []))))// prepend(square(1), prepend(square(2), prepend(square(3), [])))// prepend(square(1), prepend(square(2), prepend(9, [])))// prepend(square(1), prepend(square(2), [9]))// prepend(square(1), prepend(4, [9]))// prepend(square(1), [4, 9])// prepend(1, [4, 9])// [1, 4, 9]
I’m using brackets here to represent lists, but remember that these aren’t
arrays, but are actually the functions that were returned by prepend
.
If you’re still not sure about this, trace out every step of map(square,
empty_list)
on paper. Then trace out every step of map(square, prepend(10,
empty_list))
.
Thinking recursively like this takes some practice. I have notebooks filled with pages like this. Experienced guitarists practice new material slowly and methodically — there’s no reason programmers shouldn’t do the same. Watching the function calls expand and contract on paper can help you feel in your gut how these things work in a way that just staring at the words can’t.
filter
We’re going to start moving a bit faster now, but you should still make sure you understand everything completely before moving on. Take as much time as you need. Write things out. Run them. Get a feel for them.
The next “utility” function we’ll build on top of lists is filter
, which
takes a function and a list, and returns a new list whose elements are those in
the original that make the function return true
. Here’s an example:
varnumbers=prepend(1,prepend(2,prepend(3,empty_list)));varis_odd=function(x){returnx%2===1;}filter(is_odd,numbers);// [1, 3]
Now let’s implement filter
:
varfilter=function(fn,l){if(is_empty(l)){returnempty_list;}elseif(fn(head(l))){returnprepend(head(l),filter(fn,tail(l)));}else{returnfilter(fn,tail(l));}};
Take your time. Trace out some examples. Move on when you feel it in your gut.
and, or, not
Let’s take a slight detour to implement a few “helper” functions. Τhese don’t have anything specifically to do with lists, but we’ll need them later.
varnot=function(x){if(x){returnfalse;}else{returntrue;}};varand=function(a,b){if(a){if(b){returntrue;}else{returnfalse;}}else{returnfalse;}};varor=function(a,b){if(a){returntrue;}else{if(b){returntrue;}else{returnfalse;}}};
Javascript already has these things built in as !
, &&
, and ||
, of course,
but remember that in this thought exercise we’re trying to avoid using extra
language features if we don’t need them. How far can we scrape by on just
functions and if
statements?
One small note: these functions are just normal Javascript functions, which
means that and(a, b)
will not short-circuit like a && b
would. For our
purposes here that won’t hurt us, but it’s something to be aware of.
List Out of Lambda
Now that we’ve had a bit more practice, let’s go back to our definition of lists:
varempty_list=null;varprepend=function(el,list){returnfunction(command){if(command==="head"){returnel;}elseif(command==="tail"){returnlist;}}};varhead=function(list){returnlist("head");};vartail=function(list){returnlist("tail");};varis_empty=function(list){returnlist===null;};
There are a few things about this implementation that bother me. Our goal is to use as few language features as possible, but we’ve actually used quite a few! I count at least five:
- Functions
if
statements- Strings
- Booleans (the
true
/false
result ofis_empty
) - Equality checking (the
===
checks)
It turns out we can remove most of those things at the cost of a bit of readability (and more bending of our minds).
Let’s start by rewriting the core three functions to ditch those ugly strings,
equality checks, and even the if
statement:
varprepend=function(el,list){returnfunction(selector){returnselector(el,list);};};varhead=function(list){returnlist(function(h,t){returnh;});};vartail=function(list){returnlist(function(h,t){returnt;});};
You may want to get a snack before wrapping your brain around this one! There’s
no strings, no equality checking, no if
statements. But we still have lists!
The prepend
functions still returns a function, just like before. Remember
that in the last implementation, a “list” was really a function that knew how to
give out its head or its tail when asked for them.
This time, we’re inverting the “asking”. In this version, a “list” is “a function that will tell another function about both its head and its tail when asked”. This time the asker gets both pieces, and can decide which one they want to use.
Let’s look at the head
function:
head
takes a list and saysreturn list(...)
, which means: “Hey list, I would like you to tell all of your info to this little helper function I’m giving you”.- The list says
return ...(el, list)
, which means: “Okay helper function, here’s my head and my tail, enjoy!” - The helper function that
head
originally gave wasfunction(h, t) { return h; }
. So when the list calls it with the head and the tail as arguments, it returns the head and ignores the tail. head
takes that result and just returns it straight through back to the caller.
tail
works exactly the same way, but its helper function returns the second
argument (the tail) instead of the first.
That’s it! The equality checking and if
statements have disappeared. Can you
describe where they’ve gone? What has taken their place?
Before we move on, let’s clean up the idea of the empty list. It’s still using
null
and equality checking. Let’s remove those and make things a little more uniform.
To do this we’ll need to change the other three functions a bit as well, but if you’ve understood everything so far it shouldn’t be too bad.
varempty_list=function(selector){returnselector(undefined,undefined,true);};varprepend=function(el,list){returnfunction(selector){returnselector(el,list,false);};};varhead=function(list){returnlist(function(h,t,e){returnh;});};vartail=function(list){returnlist(function(h,t,e){returnt;});};varis_empty=function(list){returnlist(function(h,t,e){returne;});};
We’ve now made lists a bit smarter. In addition to telling the helper function
their head and tail, they also tell it “am I the empty list?”. We’ve modified
the helpers head
and tail
to accept (and ignore) this extra argument.
We then modified is_empty
to work just like head
and tail
.
Finally, we’ve redefined empty_list
to match the rest of the lists instead of
being a special, magic value. The empty list is now just like a normal one:
it’s a function that takes an “asker” and tells that asker “Hey, my head and
tail are undefined and I am the empty list”.
I used undefined
here which is technically another language feature because
it’s easier to read. Feel free to replace it with anything you want to make it
more pure. Since we’re being very careful to never call head
or tail
on the
empty list those values will never be seen anyway.
So after all that, we’ve finally implemented the building blocks of lists with only two things:
- Functions.
true
andfalse
for empty lists.
If you’re up for a challenge, think about whether you could remove that second item (and if so, are you really removing it, or just using certain features of Javascript implicitly instead of explicitly?).
A Brief Intermission
Let’s take a minute to reflect on all the code we’ve seen so far. First, we have an implementation of lists that uses only functions and booleans:
varempty_list=function(selector){returnselector(undefined,undefined,true);};varprepend=function(el,list){returnfunction(selector){returnselector(el,list,false);};};varhead=function(list){returnlist(function(h,t,e){returnh;});};vartail=function(list){returnlist(function(h,t,e){returnt;});};varis_empty=function(list){returnlist(function(h,t,e){returne;});};
From this point on, we can now ignore the details of how lists are implemented.
As long as we have head
, tail
, and prepend
we don’t need to worry about
what lists actually are under the hood.
We also built a few helper functions on top of this foundation:
varnot=function(x){if(x){returnfalse;}else{returntrue;}};varand=function(a,b){if(a){if(b){returntrue;}else{returnfalse;}}else{returnfalse;}};varor=function(a,b){if(a){returntrue;}else{if(b){returntrue;}else{returnfalse;}}};varmap=function(fn,l){if(is_empty(l)){returnempty_list;}else{returnprepend(fn(head(l)),map(fn,tail(l)));}};varfilter=function(fn,l){if(is_empty(l)){returnempty_list;}elseif(fn(head(l))){returnprepend(head(l),filter(fn,tail(l)));}else{returnfilter(fn,tail(l));}};
Before you move on, make sure all of this code is crystal clear. Come back tomorrow if you need to let it sink in. We’re about to go a lot deeper into the rabbit hole, so make sure you’re ready.
Numbers
If you look at the definitions of prepend
, head
, and tail
, they’re pretty
mind-bending. However, the definitions of map
and filter
are much more straightforward.
This is because we encapsulated the implementation of lists into the first four
functions. We did all the hard work of building lists out of almost nothing at
all and hid it behind that simple prepend
, head
, and tail
interface.
The idea of creating things from simple pieces and abstracting them into “black boxes” is one of the most important parts of both computer science and programming, so let’s take it a step further and get some more practice by implementing numbers.
What is a Number?
For this blog post we’re only going to concern ourselves with non-negative integers. Feel free to try extending all this to include negative integers if you want more.
How can we represent a number? Well we could obviously use Javascript numbers
like 14
, but that’s not very fun, and we’re trying to minimize the number of
language features we use.
One way to represent a number is a list whose length is the number. So we could
say that [1, 1, 1]
means “three”, ["cats", null]
means “two”, and []
means “zero”.
The elements themselves don’t really matter, so let’s just pick something we already have: the empty list! Let’s write out a few to get a feel for this:
varzero=empty_list;// []varone=prepend(empty_list,empty_list);// [ [] ]vartwo=prepend(empty_list,prepend(empty_list,empty_list));// [ [], [] ]
inc, dec
We’re going to want to do things with our numbers, so let’s start writing things that work with this “list of things” representation of numbers.
Our basic building blocks are going to be inc
and dec
(increment and decrement).
varinc=function(n){returnprepend(empty_list,n);};vardec=function(n){returntail(n);};
To add 1 to a number, we just push another element on the list. Soinc(inc(zero))
means “two”.
To subtract 1, we just pop off one of the elements: dec(two)
means “one”
(remember we’re ignoring negative numbers).
is_zero
When we started working with lists we used is_empty
a lot, so it’s probably
a good idea to create an is_zero
function at this point:
varis_zero=function(n){returnis_empty(n);};
Zero is just represented by the empty list, so this one is easy!
add
Adding one is easy, but we’re probably going to want to add arbitrary numbers
together. Now that we have inc
and dec
this is actually pretty easy:
varadd=function(a,b){if(is_zero(b)){returna;}else{returnadd(inc(a),dec(b));}};
This is another recursive definition. When adding two numbers, there are two possibilities:
- If
b
is zero, then anything plus zero is zero, so we can just returna
. - Otherwise, adding
a + b
is the same as adding(a + 1) + (b - 1)
.
Eventually b
will “bottom out” and return a
(which has been steadily getting
bigger as b
got smaller).
Notice how we didn’t say anything about lists here! The “numbers are lists
under the hood” idea has been encapsulated behind is_zero
, inc
, and dec
,
so we can ignore it and work at the “number” level of abstraction from here on out.
sub
Subtraction is similar to addition, but instead of increasinga
as b
gets
smaller, we decrease them both together:
varsub=function(a,b){if(is_zero(b)){returna;}else{returnsub(dec(a),dec(b));}};
Now we can say something like add(two, sub(three, two))
and the result will be
a representation of “three” in our system (which, of course, is a list of three elements).
Pause for a minute now and remember that underneath numbers are lists, and
underneath lists there’s nothing but functions. We can add and subtract
integers and underneath it all it’s just functions shuffling around, expanding
into other functions and contracting as they’re called, and this writhing mass
of lambdas somehow ends up representing 1 + 1 = 2
. That’s pretty cool!
mul, pow
For practice let’s create a way to multiply numbers:
varmul=function(a,b){if(is_zero(b)){returnzero;}else{returnadd(a,mul(a,dec(b)));}};
Building on add
makes this pretty easy. 3 * 4
is the same as 3
+ 3 + 3 + 3 + 0
. Trace out the execution on paper if things are starting to
get away from you. Carry on when you’re ready.
pow
(“power” or exponential) follows a similar structure as mul
, but instead
of adding together the copies we multiply them, and our base is one instead of zero:
varpow=function(a,b){if(is_zero(b)){returnone;}else{returnmul(a,pow(a,dec(b)));}};
is_equal
A common thing to do with numbers is to check if two are equal, so let’s write that:
varis_equal=function(n,m){if(and(is_zero(n),is_zero(m))){returntrue;}elseif(or(is_zero(n),is_zero(m))){returnfalse;}else{returnis_equal(dec(n),dec(m));}};
There are three cases here:
- If both numbers are zero, they are equal.
- If only one number is zero (but not both, or the first case would have caught it), then they are not equal.
- Otherwise, subtract one from each and try again.
When calling this function with two non-zero numbers, both will be decremented in tandem until one of them bottoms out at zero first, or until they bottom out at the same time.
less_than, greater_than
We can take a similar approach to implementing less_than
:
varless_than=function(a,b){if(and(is_zero(a),is_zero(b))){returnfalse;}elseif(is_zero(a)){returntrue;}elseif(is_zero(b)){returnfalse;}else{returnless_than(dec(a),dec(b));}};
The difference here is that we have four cases.
- If both numbers are zero, then
a
is not less thanb
. - Otherwise if
a
is zero (and we knowb
isn’t) then yes,a
is less thanb
. - Otherwise if
b
is zero (and we know thata
isn’t) then no,a
cannot be less thanb
(remember that we’re ignoring negative numbers). - Otherwise decrement both and try again.
Once again, both numbers race to bottom out, and the outcome is decided by which one bottoms out first.
We could do something similar for greater_than
, but let’s do it the easy way instead:
vargreater_than=function(a,b){returnless_than(b,a);};
div, mod
Once we have less_than
we’re ready to implement division and remainders:
vardiv=function(a,b){if(less_than(a,b)){returnzero;}else{returninc(div(sub(a,b),b));}};varrem=function(a,b){if(less_than(a,b)){returna;}else{returnrem(sub(a,b),b);}};
This pair is a bit more complicated than the three other basic operations because we can’t deal with negative numbers. Make sure you understand how it works.
Full Circle
At this point, we have a (very basic) working system of numbers built on top of lists. Let’s chase our tails a bit and implement a few more list functions that use numbers.
nth
To get the Nth item in a list, we just pop things off of it as we decrement N until we hit zero:
varnth=function(l,n){if(is_zero(n)){returnhead(l);}else{returnnth(tail(l),dec(n));}};
Under the hood there are really two lists getting things popped off as we
iterate, because n
is a number, which is a list, and dec
pops things off.
But it’s much easier to read when we’ve abstracted away the representation of
numbers, don’t you think?
drop, take
Two handy functions for working with lists are drop
and take
.
drop(l, three)
will return the list with the first three elements removed.
take(l, three)
will return the list containing only the first three elements.
vardrop=function(l,n){if(is_zero(n)){returnl;}else{returndrop(tail(l),dec(n));}};vartake=function(l,n){if(is_zero(n)){returnempty_list;}else{returnprepend(head(l),take(tail(l),dec(n)));}};
slice
Slicing a list is easy now that we have drop
, take
, and the ability to subtract numbers:
varslice=function(l,start,end){returntake(drop(l,start),sub(end,start));};
First we drop up to the start, then take enough to get us to the end.
length
We can define length
recursively like everything else:
varlength=function(l){if(is_empty(l)){returnzero;}else{returninc(length(tail(l)));}};
The length of the empty list is zero, and the length of any non-empty list is one plus the length of its tail.
If your mind isn’t in knots by this point, consider the following:
- Lists are made of functions.
- Numbers are made of lists whose length represents the number.
length
is a function that takes a list (which is a function) and returns the length as a number (a list whose length represents the number).- We only just now got around to defining
length
even though we’ve been using numbers (which use the length of a list to represent a number) for a while now!
Are you dizzy yet? If not:
varmylist=prepend(empty_list,prepend(empty_list,empty_list));varmylistlength=length(mylist);
mylist
is a list of two empty lists.
mylistlength
is the length of mylist
…
which is “two”…
which is represented by a list of two empty lists…
which is mylist
itself!
Conclusion
If you liked this twisty little story, I highly recommend you check out The Little Schemer. It was one of the first books that really changed how I thought about programming. Don’t be put off by the fact that it uses Scheme — the language doesn’t really matter.
I’ve also created a gist with all the code. Feel free to fork it and use it for practice.
You could add some more utility functions for practice writing recursively:
append
to add an item to the end of a list.concat
to concatenate two lists.min
andmax
which take two numbers and return the minimum/maximum one.remove
, which is like filter except it only leaves the elements that returnfalse
for the predicate.contains_number
, which checks if a specific number is inside a list of numbers.
Or if you want something more challenging, try implementing bigger concepts on top of the current ones:
- Negative numbers.
- Non-negative rational numbers.
- Negative rational numbers.
- Association lists (a data structure that associates keys with values).
Remember: the point is not to create something that runs well on a physical computer. Instead of thinking about how to make a particular combination of transistors and circuits have the right voltages, think about “computing” in the beautiful, perfect, abstract sense.