Quantcast
Channel: Hacker News 50
Viewing all articles
Browse latest Browse all 9433

List Out of Lambda / Steve Losh

$
0
0

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 of is_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 says return 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 was function(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 and false 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 return a.
  • 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 than b.
  • Otherwise if a is zero (and we know b isn’t) then yes, a is less thanb.
  • Otherwise if b is zero (and we know that a isn’t) then no, a cannot be less than b (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 and max 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.


Viewing all articles
Browse latest Browse all 9433

Trending Articles