TranslateProject/sources/tech/20170320 Whiteboard problems in pure Lambda Calculus.md
DarkSun 1934f43476 选题: 20170320 Whiteboard problems in pure Lambda Calculus
sources/tech/20170320 Whiteboard problems in pure Lambda Calculus.md
2019-09-17 13:10:58 +08:00

53 KiB
Raw Blame History

Whiteboard problems in pure Lambda Calculus

My team at Vivint, the Space Monkey group, stopped doing whiteboard interviews a while ago. We certainly used to do them, but weve transitioned to homework problems or actually just hiring a candidate as a short term contractor for a day or two to solve real work problems and see how that goes. Whiteboard interviews are kind of like Festivus but in a bad way: you get the feats of strength and then the airing of grievances. Unfortunately, modern programming is nothing like writing code in front of a roomful of strangers with only a whiteboard and a marker, so its probably not best to optimize for that.

Nonetheless, Kyles recent (wonderful, amazing) post titled acing the technical interview got me thinking about fun ways to approach whiteboard problems as an interviewee. Kyles Church-encodings made me wonder how many “standard” whiteboard problems you could solve in pure lambda calculus. If this isnt seen as a feat of strength by your interviewers, there will certainly be some airing of grievances.

➡️ Update: Ive made a lambda calculus web playground so you can run lambda calculus right in your browser! Ive gone through and made links to examples in this post with it. Check it out at https://jtolds.github.io/sheepda/

Lambda calculus

Wait, what is lambda calculus? Did I learn that in high school?

Big-C “Calculus” of course usually refers to derivatives, integrals, Taylor series, etc. You might have learned about Calculus in high school, but this isnt that.

More generally, a little-c “calculus” is really just any system of calculation. The lambda calculus is essentially a formalization of the smallest set of primitives needed to make a completely Turing-complete programming language. Expressions in the language can only be one of three things.

  • An expression can define a function that takes exactly one argument (no more, no less) and then has another expression as the body.
  • An expression can call a function by applying two subexpressions.
  • An expression can reference a variable.

Here is the entire grammar:

<expr> ::= <variable>
         | `λ` <variable> `.` <expr>
         | `(` <expr> <expr> `)`

Thats it. Theres nothing else you can do. There are no numbers, strings, booleans, pairs, structs, anything. Every value is a function that takes one argument. All variables refer to these functions, and all functions can do is return another function, either directly, or by calling yet another function. Theres nothing else to help you.

To be honest, its a little surprising that this is even Turing-complete. How do you do branches or loops or recursion? This seems too simple to work, right?

A common whiteboard problem is the fizz buzz problem. The goal is to write a function that prints out all the numbers from 0 to 100, but instead of printing numbers divisible by 3 it prints “fizz”, and instead of printing numbers divisible by 5 it prints “buzz”, and in the case of both it prints “fizzbuzz”. Its a simple toy problem but its touted as a good whiteboard problem because evidently many self-proclaimed programmers cant solve it. Maybe part of that is cause whiteboard problems suck? I dunno.

Anyway, heres fizz buzz in pure lambda calculus:

(λU.(λY.(λvoid.(λ0.(λsucc.(λ+.(λ*.(λ1.(λ2.(λ3.(λ4.(λ5.(λ6.(λ7.(λ8.(λ9.(λ10.(λnum.(λtrue.(λfalse.(λif.(λnot.(λand.(λor.(λmake-pair.(λpair-first.(λpair-second.(λzero?.(λpred.(λ-.(λeq?.(λ/.(λ%.(λnil.(λnil?.(λcons.(λcar.(λcdr.(λdo2.(λdo3.(λdo4.(λfor.(λprint-byte.(λprint-list.(λprint-newline.(λzero-byte.(λitoa.(λfizzmsg.(λbuzzmsg.(λfizzbuzzmsg.(λfizzbuzz.(fizzbuzz (((num 1) 0) 1)) λn.((for n) λi.((do2 (((if (zero? ((% i) 3))) λ_.(((if (zero? ((% i) 5))) λ_.(print-list fizzbuzzmsg)) λ_.(print-list fizzmsg))) λ_.(((if (zero? ((% i) 5))) λ_.(print-list buzzmsg)) λ_.(print-list (itoa i))))) (print-newline nil)))) ((cons (((num 0) 7) 0)) ((cons (((num 1) 0) 5)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) ((cons (((num 0) 9) 8)) ((cons (((num 1) 1) 7)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))))))) ((cons (((num 0) 6) 6)) ((cons (((num 1) 1) 7)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))) ((cons (((num 0) 7) 0)) ((cons (((num 1) 0) 5)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))) λn.(((Y λrecurse.λn.λresult.(((if (zero? n)) λ_.(((if (nil? result)) λ_.((cons zero-byte) nil)) λ_.result)) λ_.((recurse ((/ n) 10)) ((cons ((+ zero-byte) ((% n) 10))) result)))) n) nil)) (((num 0) 4) 8)) λ_.(print-byte (((num 0) 1) 0))) (Y λrecurse.λl.(((if (nil? l)) λ_.void) λ_.((do2 (print-byte (car l))) (recurse (cdr l)))))) PRINT_BYTE) λn.λf.((((Y λrecurse.λremaining.λcurrent.λf.(((if (zero? remaining)) λ_.void) λ_.((do2 (f current)) (((recurse (pred remaining)) (succ current)) f)))) n) 0) f)) λa.do3) λa.do2) λa.λb.b) λl.(pair-second (pair-second l))) λl.(pair-first (pair-second l))) λe.λl.((make-pair true) ((make-pair e) l))) λl.(not (pair-first l))) ((make-pair false) void)) λm.λn.((- m) ((* ((/ m) n)) n))) (Y λ/.λm.λn.(((if ((eq? m) n)) λ_.1) λ_.(((if (zero? ((- m) n))) λ_.0) λ_.((+ 1) ((/ ((- m) n)) n)))))) λm.λn.((and (zero? ((- m) n))) (zero? ((- n) m)))) λm.λn.((n pred) m)) λn.(((λn.λf.λx.(pair-second ((n λp.((make-pair (f (pair-first p))) (pair-first p))) ((make-pair x) x))) n) succ) 0)) λn.((n λ_.false) true)) λp.(p false)) λp.(p true)) λx.λy.λt.((t x) y)) λa.λb.((a true) b)) λa.λb.((a b) false)) λp.λt.λf.((p f) t)) λp.λa.λb.(((p a) b) void)) λt.λf.f) λt.λf.t) λa.λb.λc.((+ ((+ ((* ((* 10) 10)) a)) ((* 10) b))) c)) (succ 9)) (succ 8)) (succ 7)) (succ 6)) (succ 5)) (succ 4)) (succ 3)) (succ 2)) (succ 1)) (succ 0)) λm.λn.λx.(m (n x))) λm.λn.λf.λx.((((m succ) n) f) x)) λn.λf.λx.(f ((n f) x))) λf.λx.x) λx.(U U)) (U λh.λf.(f λx.(((h h) f) x)))) λf.(f f))

➡️ Try it out in your browser!

(This program expects a function to be defined called PRINT_BYTE which takes a Church-encoded numeral, turns it into a byte, writes it to stdout, and then returns the same Church-encoded numeral. Expecting a function that has side-effects might arguably disqualify this from being pure, but its definitely arguable.)

Dont be deceived! I said there were no native numbers or lists or control structures in lambda calculus and I meant it. 0, 7, if, and + are all variables that represent functions and have to be constructed before they can be used in the code block above.

What? Whats happening here?

Okay lets start over and build up to fizz buzz. Were going to need a lot. Were going to need to build up concepts of numbers, logic, and lists all from scratch. Ask your interviewers if theyre comfortable cause this might be a while.

Here is a basic lambda calculus function:

λx.x

This is the identity function and it is equivalent to the following Javascript:

function(x) { return x; }

It takes an argument and returns it! We can call the identity function with another value. Function calling in many languages looks like f(x), but in lambda calculus, it looks like (f x).

(λx.x y)

This will return y. Once again, heres equivalent Javascript:

function(x) { return x; }(y)

Aside: If youre already familiar with lambda calculus, my formulation of precedence is such that (λx.x y) is not the same as λx.(x y). (λx.x y) applies y to the identity function λx.x, and λx.(x y) is a function that applies y to its argument x. Perhaps not what youre used to, but the parser was way more straightforward, and programming with it this way seems a bit more natural, believe it or not.

Okay, great. We can call functions. What if we want to pass more than one argument?

Currying

Imagine the following Javascript function:

let s1 = function(f, x) { return f(x); }

We want to call it with two arguments, another function and a value, and we want the function to then be called on the value, and have its result returned. Can we do this while using only one argument?

Currying is a technique for dealing with this. Instead of taking two arguments, take the first argument and return another function that takes the second argument. Heres the Javascript:

let s2 = function(f) {
  return function(x) {
    return f(x);
  }
};

Now, s1(f, x) is the same as s2(f)(x). So the equivalent lambda calculus for s2 is then

λf.λx.(f x)

Calling this function with g for f and y for x is like so:

((s2 g) y)

or

((λf.λx.(f x) g) y)

The equivalent Javascript here is:

function(f) {
  return function(x) {
    f(x)
  }
}(g)(y)

Numbers

Since everything is a function, we might feel a little stuck with what to do about numbers. Luckily, Alonzo Church already figured it out for us! When you have a number, often what you want to do is represent how many times you might do something.

So lets represent a number as how many times well apply a function to a value. This is called a Church numeral. If we have f and x, 0 will mean we dont call f at all, and just return x. 1 will mean we call f one time, 2 will mean we call f twice, and so on.

Here are some definitions! (N.B.: assignment isnt actually part of lambda calculus, but it makes writing down definitions easier)

0 = λf.λx.x

Here, 0 takes a function f, a value x, and never calls f. It just returns x. f is called 0 times.

1 = λf.λx.(f x)

Like 0, 1 takes f and x, but here it calls f exactly once. Lets see how this continues for other numbers.

2 = λf.λx.(f (f x))
3 = λf.λx.(f (f (f x)))
4 = λf.λx.(f (f (f (f x))))
5 = λf.λx.(f (f (f (f (f x)))))

5 is a function that takes f, x, and calls f 5 times!

Okay, this is convenient, but how are we going to do math on these numbers?

Successor

Lets make a successor function that takes a number and returns a new number that calls f just one more time.

succ = λn.  λf.λx.(f ((n f) x))

succ is a function that takes a Church-encoded number, n. The spaces after λn. are ignored. I put them there to indicate that we expect to usually call succ with one argument, curried or no. succ then returns another Church-encoded number, λf.λx.(f ((n f) x)). What is it doing? Lets break it down.

  • ((n f) x) looks like that time we needed to call a function that took two “curried” arguments. So were calling n, which is a Church numeral, with two arguments, f and x. This is going to call f n times!
  • (f ((n f) x)) This is calling f again, one more time, on the result of the previous value.

So does succ work? Lets see what happens when we call (succ 1). We should get the 2 we defined earlier!

   (succ 1)
-> (succ λf.λx.(f x))                   # resolve the variable 1
-> (λn.λf.λx.(f ((n f) x)) λf.λx.(f x)) # resolve the variable succ
-> λf.λx.(f ((λf.λx.(f x) f) x))        # call the outside function. replace n
                                        # with the argument

let's sidebar and simplify the subexpression
   (λf.λx.(f x) f)
-> λx.(f x)                             # call the function, replace f with f!

now we should be able to simplify the larger subexpression
   ((λf.λx.(f x) f) x)
-> (λx.(f x) x)                         # sidebar above
-> (f x)                                # call the function, replace x with x!

let's go back to the original now
   λf.λx.(f ((λf.λx.(f x) f) x))
-> λf.λx.(f (f x))                      # subexpression simplification above

and done! That last line is identical to the 2 we defined originally! It calls f twice.

Math

Now that we have the successor function, if your interviewers havent checked out, tell them that fizz buzz isnt too far away now; we have Peano Arithmetic! They can then check their interview bingo cards and see if theyve increased their winnings.

No but for real, since we have the successor function, we can now easily do addition and multiplication, which we will need for fizz buzz.

First, recall that a number n is a function that takes another function f and an initial value x and applies f n times. So if you have two numbers m and n, what you want to do is apply succ to m n times!

+ = λm.λn.((n succ) m)

Here, + is a variable. If its not a lambda expression or a function call, its a variable!

Multiplication is similar, but instead of applying succ to m n times, were going to add m to 0 n times.

First, note that if ((+ m) n) is adding m and n, then that means that (+ m) is a function that adds m to its argument. So we want to apply the function (+ m) to 0 n times.

* = λm.λn.((n (+ m)) 0)

Yay! We have multiplication and addition now.

Logic

Were going to need booleans and if statements and logic tests and so on. So, lets talk about booleans. Recall how with numbers, what we kind of wanted with a number n is to do something n times. Similarly, what we want with booleans is to do one of two things, either/or, but not both. Alonzo Church to the rescue again.

Lets have booleans be functions that take two arguments (curried of course), where the true boolean will return the first option, and the false boolean will return the second.

true  = λt.λf.t
false = λt.λf.f

So that we can demonstrate booleans, were going to define a simple sample function called zero? that returns true if a number n is zero, and false otherwise:

zero? = λn.((n λ_.false) true)

To explain: if we have a Church numeral for 0, it will call the first argument it gets called with 0 times and just return the second argument. In other words, 0 will just return the second argument and thats it. Otherwise, any other number will call the first argument at least once. So, zero? will take n and give it a function that throws away its argument and always returns false whenever its called, and start it off with true. Only zero values will return true.

➡️ Try it out in your browser!

We can now write an if' function to make use of these boolean values. if' will take a predicate value p (the boolean) and two options a and b.

if' = λp.λa.λb.((p a) b)

You can use it like this:

((if' (zero? n)
      (something-when-zero x))
      (something-when-not-zero y))

One thing thats weird about this construction is that the interpreter is going to evaluate both branches (my lambda calculus interpreter is eager instead of lazy). Both something-when-zero and something-when-not-zero are going to be called to determine what to pass in to if'. To make it so that we dont actually call the function in the branch we dont want to run, lets protect the logic in another function. Well name the argument to the function _ to indicate that we want to just throw it away.

((if (zero? n)
     λ_. (something-when-zero x))
     λ_. (something-when-not-zero y))

This means were going to have to make a new if function that calls the correct branch with a throwaway argument, like 0 or something.

if = λp.λa.λb.(((p a) b) 0)

Okay, now we have booleans and if!

Currying part deux

At this point, you might be getting sick of how calling something with multiple curried arguments involves all these extra parentheses. ((f a) b) is annoying, cant we just do (f a b)?

Its not part of the strict grammar, but my interpreter makes this small concession. (a b c) will be expanded to ((a b) c) by the parser. (a b c d) will be expanded to (((a b) c) d) by the parser, and so on.

So, for the rest of the post, for ease of explanation, Im going to use this syntax sugar. Observe how using if changes:

(if (zero? n)
    λ_. (something-when-zero x)
    λ_. (something-when-not-zero y))

Its a little better.

More logic

Lets talk about and, or, and not!

and returns true if and only if both a and b are true. Lets define it!

and = λa.λb.
  (if (a)
      λ_. b
      λ_. false)

or returns true if a is true or if b is true:

or = λa.λb.
  (if (a)
      λ_. true
      λ_. b)

not just returns the opposite of whatever it was given:

not = λa.
  (if (a)
      λ_. false
      λ_. true)

It turns out these can be written a bit more simply, but theyre basically doing the same thing:

and = λa.λb.(a b false)
or = λa.λb.(a true b)
not = λp.λt.λf.(p f t)

➡️ Try it out in your browser!

Pairs!

Sometimes its nice to keep data together. Lets make a little 2-tuple type! We want three functions. We want a function called make-pair that will take two arguments and return a “pair”, we want a function called pair-first that will return the first element of the pair, and we want a function called pair-second that will return the second element. How can we achieve this? Youre almost certainly in the interview room alone, but nows the time to yell “Alonzo Church”!

make-pair = λx.λy. λa.(a x y)

make-pair is going to take two arguments, x and y, and they will be the elements of the pair. The pair itself is a function that takes an “accessor” a that will be given x and y. All a has to do is take the two arguments and return the one it wants.

Here is someone making a pair with variables 1 and 2:

(make-pair 1 2)

This returns:

λa.(a 1 2)

Theres a pair! Now we just need to access the values inside.

Remember how true takes two arguments and returns the first one and false takes two arguments and returns the second one?

pair-first = λp.(p true)
pair-second = λp.(p false)

pair-first is going to take a pair p and give it true as the accessor a. pair-second is going to give the pair false as the accessor.

Voilà, you can now store 2-tuples of values and recover the data from them.

➡️ Try it out in your browser!

Lists!

Were going to construct linked lists. Each list item needs two things: the value at the current position in the list and a reference to the rest of the list.

One additional caveat is we want to be able to identify an empty list, so were going to store whether or not the current value is the end of a list as well. In LISP-based programming languages, the end of the list is the special value nil, and checking if weve hit the end of the list is accomplished with the nil? predicate.

Because we want to distinguish nil from a list with a value, were going to store three things in each linked list item. Whether or not the list is empty, and if not, the value and the rest of the list. So we need a 3-tuple.

Once we have pairs, other-sized tuples are easy. For instance, a 3-tuple is just one pair with another pair inside for one of the slots.

For each list element, well store:

[not-empty [value rest-of-list]]

As an example, a list element with a value of 1 would look like:

[true [1 remainder]]

whereas nil will look like

[false whatever]

That second part of nil just doesnt matter.

First, lets define nil and nil?:

nil = (make-pair false false)
nil? = λl. (not (pair-first l))

The important thing about nil is that the first element in the pair is false.

Now that we have an empty list, lets define how to add something to the front of it. In LISP-based languages, the operation to construct a new list element is called cons, so well call this cons, too.

cons will take a value and an existing list and return a new list with the given value at the front of the list.

cons = λvalue.λlist.
  (make-pair true (make-pair value list))

cons is returning a pair where, unlike nil, the first element of the pair is true. This represents that theres something in the list here. The second pair element is what we wanted in our linked list: the value at the current position, and a reference to the rest of the list.

So how do we access things in the list? Lets define two functions called head and tail. head is going to return the value at the front of the list, and tail is going to return everything but the front of the list. In LISP-based languages, these functions are sometimes called car and cdr for surprisingly esoteric reasons. head and tail have undefined behavior here when called on nil, so lets just assume nil? is false for the list and keep going.

head = λlist. (pair-first (pair-second list))
tail = λlist. (pair-second (pair-second list))

Both head and tail first get (pair-second list), which returns the tuple that has the value and reference to the remainder. Then, they use either pair-first or pair-second to get the current value or the rest of the list.

Great, we have lists!

➡️ Try it out in your browser!

Recursion and loops

Lets make a simple function that sums up a list of numbers.

sum = λlist.
  (if (nil? list)
      λ_. 0
      λ_. (+ (head list) (sum (tail list))))

If the list is empty, lets return 0. If the list has an element, lets add that element to the sum of the rest of the list. Recursion is a cornerstone tool of computer science, and being able to assume a solution to a subproblem to solve a problem is super neat!

Okay, except, this doesnt work like this in lambda calculus. Remember how I said assignment wasnt something that exists in lambda calculus? If you have:

x = y
<stuff>

This really means you have:

(λx.<stuff> y)

In the case of our sum definition, we have:

(λsum.
  <your-program>

 λlist.
  (if (nil? list)
      λ_. 0
      λ_. (+ (head list) (sum (tail list)))))

What that means is sum doesnt have any access to itself. It cant call itself like weve written, because when it tries to call sum, its undefined!

This is a pretty crushing blow, but it turns out theres a mind bending and completely unexpected trick the universe has up its sleeve.

Assume we wrote sum so that it takes two arguments. A reference to something like sum well call helper and then the list. If we could figure out how to solve the recursion problem, then we could use this sum. Lets do that.

sum = λhelper.λlist.
  (if (nil? list)
      λ_. 0
      λ_. (+ (head list) (helper (tail list))))

But hey! When we call sum, we have a reference to sum then! Lets just give sum itself before the list.

(sum sum list)

This seems promising, but unfortunately now the helper invocation inside of sum is broken. helper is just sum and sum expects a reference to itself. Lets try again, changing the helper call:

sum = λhelper.λlist.
  (if (nil? list)
      λ_. 0
      λ_. (+ (head list) (helper helper (tail list))))

(sum sum list)

We did it! This actually works! We engineered recursion out of math! At no point does sum refer to itself inside of itself, and yet we managed to make a recursive function anyways!

➡️ Try it out in your browser!

Despite the minor miracle weve just performed, weve now ruined how we program recursion to involve calling recursive functions with themselves. This isnt the end of the world, but its a little annoying. Luckily for us, theres a function that cleans this all right up called the Y combinator.

The Y combinator is probably now more famously known as a startup incubator, or perhaps even more so as the domain name for one of the most popular sites that has a different name than its URL, Hacker News, but fixed point combinators such as the Y combinator have had a longer history.

The Y combinator can be defined in different ways, but definition Im using is:

Y = λf.(λx.(x x) λx.(f λy.((x x) y)))

You might consider reading more about how the Y combinator can be derived from an excellent tutorial such as this one or this one.

Anyway, Y will make our original sum work as expected.

sum = (Y λhelper.λlist.
  (if (nil? list)
      λ_. 0
      λ_. (+ (head list) (helper (tail list)))))

We can now call (sum list) without any wacky doubling of the function name, either inside or outside of the function. Hooray!

➡️ Try it out in your browser!

More math

“Get ready to do more math! We now have enough building blocks to do subtraction, division, and modulo, which well need for fizz buzz,” you tell the security guards that are approaching you.

Just like addition, before we define subtraction well define a predecessor function. Unlike addition, the predecessor function pred is much more complicated than the successor function succ.

The basic idea is were going to create a pair to keep track of the previous value. Well start from zero and build up n but also drag the previous value such that at n we also have n - 1. Notably, this solution does not figure out how to deal with negative numbers. The predecessor of 0 will be 0, and negatives will have to be dealt with some other time and some other way.

First, well make a helper function that takes a pair of numbers and returns a new pair where the first number in the old pair is the second number in the new pair, and the new first number is the successor of the old first number.

pred-helper = λpair.
  (make-pair (succ (pair-first pair)) (pair-first pair))

Make sense? If we call pred-helper on a pair [0 0], the result will be [1 0]. If we call it on [1 0], the result will be [2 1]. Essentially this helper slides older numbers off to the right.

Okay, so now were going to call pred-helper n times, with a starting pair of [0 0], and then get the second value, which should be n - 1 when were done, from the pair.

pred = λn.
  (pair-second (n pred-helper (make-pair 0 0)))

We can combine these two functions now for the full effect:

pred = λn.
  (pair-second
    (n
     λpair.(make-pair (succ (pair-first pair)) (pair-first pair))
     (make-pair 0 0)))

➡️ Try it out in your browser!

Now that we have pred, subtraction is easy! To subtract n from m, were going to apply pred to m n times.

- = λm.λn.(n pred m)

Keep in mind that if n is equal to or greater than m, the result of (- m n) will be zero, since there are no negative numbers and the predecessor of 0 is 0. This fact means we can implement some new logic tests. Lets make (ge? m n) return true if m is greater than or equal to n and make (le? m n) return true if m is less than or equal to n.

ge? = λm.λn.(zero? (- n m))
le? = λm.λn.(zero? (- m n))

If we have greater-than-or-equal-to and less-than-or-equal-to, then we can make equal!

eq? = λm.λn.(and (ge? m n) (le? m n))

Now we have enough for integer division! The idea for integer division of n and m is we will keep count of the times we can subtract m from n without going past zero.

/ = (Y λ/.λm.λn.
  (if (eq? m n)
      λ_. 1
      λ_. (if (le? m n)
              λ_. 0
              λ_. (+ 1 (/ (- m n) n)))))

Once we have subtraction, multiplication, and integer division, we can create modulo.

% = λm.λn. (- m (* (/ m n) n))

➡️ Try it out in your browser!

Aside about performance

You might be wondering about performance at this point. Every time we subtract one from 100, we count up from 0 to 100 to generate 99. This effect compounds itself for division and modulo. The truth is that Church numerals and other encodings arent very performant! Just like how tapes in Turing machines arent a particularly efficient way to deal with data, Church encodings are most interesting from a theoretical perspective for proving facts about computation.

That doesnt mean we cant make things faster though!

Lambda calculus is purely functional and side-effect free, which means that all sorts of optimizations can applied. Functions can be aggressively memoized. In other words, once a specific function and its arguments have been computed, theres no need to compute them ever again. The result of that function will always be the same anyways. Further, functions can be computed lazily and only if needed. What this means is if a branch of your programs execution renders a result thats never used, the compiler can decide to just not run that part of the program and end up with the exact same result.

My interpreter does have side effects, since programs written in it can cause the system to write output to the user via the special built-in function PRINT_BYTE. As a result, I didnt choose lazy evaluation. The only optimization I chose was aggressive memoization for all functions that are side-effect free. The memoization still has room for improvement, but the result is much faster than a naive implementation.

Output

“Were rounding the corner on fizz buzz!” you shout at the receptionist as security drags you around the corner on the way to the door. “We just need to figure out how to communicate results to the user!”

Unfortunately, lambda calculus cant communicate with your operating system kernel without some help, but a small concession is all we need. Sheepda provides a single built-in function PRINT_BYTE. PRINT_BYTE takes a number as its argument (a Church encoded numeral) and prints the corresponding byte to the configured output stream (usually stdout).

With PRINT_BYTE, were going to need to reference a number of different ASCII bytes, so we should make writing numbers in code easier. Earlier we defined numbers 0 - 5, so lets start and define numbers 6 - 10.

6 = (succ 5)
7 = (succ 6)
8 = (succ 7)
9 = (succ 8)
10 = (succ 9)

Now lets define a helper to create three digit decimal numbers.

num = λa.λb.λc.(+ (+ (* (* 10 10) a) (* 10 b)) c)

The newline byte is decimal 10. Heres a function to print newlines!

print-newline = λ_.(PRINT_BYTE (num 0 1 0))

Doing multiple things

Now that we have this PRINT_BYTE function, we have functions that can cause side-effects. We want to call PRINT_BYTE but we dont care about its return value. We need a way to call multiple functions in sequence.

What if we make a function that takes two arguments and throws away the first one again?

do2 = λ_.λx.x

Heres a function to print every value in a list:

print-list = (Y λrecurse.λlist.
    (if (nil? list)
        λ_. 0
        λ_. (do2 (PRINT_BYTE (head list))
                 (recurse (tail list)))))

And heres a function that works like a for loop. It calls f with every number from 0 to n. It uses a small helper function that continues to call itself until i is equal to n, and starts i off at 0.

for = λn.λf.(
  (Y λrecurse.λi.
    (if (eq? i n)
        λ_. void
        λ_. (do2 (f i)
                 (recurse (succ i)))))
  0)

Converting an integer to a string

The last thing we need to complete fizz buzz is a function that turns a number into a string of bytes to print. You might have noticed the print-num calls in some of the web-based examples above. Were going to see how to make it! Writing this function is sometimes a whiteboard problem in its own right. In C, this function is called itoa, for integer to ASCII.

Heres an example of how it works. Imagine the number were converting to bytes is 123. We can get the 3 out by doing (% 123 10), which will be 3. Then we can divide by 10 to get 12, and then start over. (% 12 10) is 2. Well loop down until we hit zero.

Once we have a number, we can convert it to ASCII by adding the value of the '0' ASCII byte. Then we can make a list of ASCII bytes for use with print-list.

zero-char = (num 0 4 8)  # the ascii code for the byte that represents 0.

itoa = λn.(
  (Y λrecurse.λn.λresult.
    (if (zero? n)
        λ_. (if (nil? result)
                λ_. (cons zero-char nil)
                λ_. result)
        λ_. (recurse (/ n 10) (cons (+ zero-char (% n 10)) result))))
  n nil)

print-num = λn.(print-list (itoa n))

Fizz buzz

“Here we go,” you shout at the building you just got kicked out of, “heres how you do fizz buzz.”

First, we need to define three strings: “Fizz”, “Buzz”, and “Fizzbuzz”.

fizzmsg =     (cons (num 0 7 0)  # F
              (cons (num 1 0 5)  # i
              (cons (num 1 2 2)  # z
              (cons (num 1 2 2)  # z
              nil))))
buzzmsg =     (cons (num 0 6 6)  # B
              (cons (num 1 1 7)  # u
              (cons (num 1 2 2)  # z
              (cons (num 1 2 2)  # z
              nil))))
fizzbuzzmsg = (cons (num 0 7 0)  # F
              (cons (num 1 0 5)  # i
              (cons (num 1 2 2)  # z
              (cons (num 1 2 2)  # z
              (cons (num 0 9 8)  # b
              (cons (num 1 1 7)  # u
              (cons (num 1 2 2)  # z
              (cons (num 1 2 2)  # z
              nil))))))))

Okay, now lets define a function that will run from 0 to n and output numbers, fizzes, and buzzes:

fizzbuzz = λn.
  (for n λi.
    (do2
      (if (zero? (% i 3))
          λ_. (if (zero? (% i 5))
                  λ_. (print-list fizzbuzzmsg)
                  λ_. (print-list fizzmsg))
          λ_. (if (zero? (% i 5))
                  λ_. (print-list buzzmsg)
                  λ_. (print-list (itoa i))))
      (print-newline 0)))

Lets do the first 20!

(fizzbuzz (num 0 2 0))

➡️ Try it out in your browser!

Reverse a string

“ENCORE!” you shout to no one as the last cars pull out of the company parking lot. Everyones gone home but this is your last night before the restraining order goes through.

reverse-list = λlist.(
  (Y λrecurse.λold.λnew.
    (if (nil? old)
        λ_.new
        λ_.(recurse (tail old) (cons (head old) new))))
  list nil)

➡️ Try it out in your browser!

Sheepda

As I mentioned, I wrote a lambda calculus interpreter called Sheepda for playing around. By itself its pretty interesting if youre interested in learning more about how to write programming language interpreters. Lambda calculus is as simple of a language as you can make, so the interpreter is very simple itself!

Its written in Go and thanks to GopherJS its what powers the web playground.

There are some fun projects if someones interested in getting more involved. Using the library to prune lambda expression trees and simplify expressions if possible would be a start! Im sure my fizz buzz implementation isnt as minimal as it could be, and playing code golf with it would be pretty neat!

Feel free to fork https://github.com/jtolds/sheepda/, star it, bop it, twist it, or even pull it!


via: https://www.jtolio.com/2017/03/whiteboard-problems-in-pure-lambda-calculus

作者:jtolio.com 选题:lujun9972 译者:译者ID 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出