blog-static/content/blog/stack_recursion.md

424 lines
15 KiB
Markdown
Raw Permalink Normal View History

2020-03-06 21:28:09 -08:00
---
title: Creating Recursive Functions in a Stack Based Language
date: 2020-03-06T17:56:55-08:00
2020-03-06 23:06:45 -08:00
tags: ["Programming Languages"]
2020-03-06 21:28:09 -08:00
---
{{< stack_css >}}
2020-03-06 21:28:09 -08:00
In CS 381, Programming Language Fundamentals, many students chose to
implement a stack based language. Such languages are very neat,
but two of the requirements for such languages may, at first,
seem somewhat hard to satisfy:
> Recursion/loops, . . . [and] . . . Procedures/functions with arguments (or some other abstraction mechanism)
A while-loop makes enough sense. The most straightforward way to implement such a loop
is to keep reading a boolean from the stack, and, if that boolean is true, running
some sequence of instructions. But while loops do not give you procedures - they
are not a sufficiently powerful abstraction mechanism for this assignment. So, we
turn to functions.
The first instinct in implementing functions is to fall back to the tried-and-true
method of introducing more global state: we have a stack, but why don't we also
add a mapping from function names to their definitions (an environment)? This
works, but I feel like it goes somewhat against the whole idea of a stack-based
language. We can do everything we need to do, entirely on the stack!
### A Toy Language
To make this post more concrete, let's define a small language. Small enough
that it's easy to reason about, but complex enough to support functions. I won't
be giving a Haskell-encoded abstract syntax definition - rather, let's work from
concrete syntax. How about something like:
{{< latex >}}
\begin{aligned}
\textit{cmd} ::= \; & \text{Pop} \; n\\
| \; & \text{Slide} \; n \\
| \; & \text{Offset} \; n \\
| \; & \text{Eq} \\
| \; & \text{PushI} \; i \\
| \; & \text{Add} \\
| \; & \text{Mul} \\
| \; & \textbf{if} \; \{ \textit{cmd}* \} \; \textbf{else} \; \{ \textit{cmd}* \} \\
| \; & \textbf{func} \; \{ \textit{cmd}* \} \\
\ \; & \textbf{Call}
\end{aligned}
{{< /latex >}}
Let's informally define the meanings of each of the described commands:
1. \(\text{Pop} \; n\): Removes the top \(n\) elements from the stack.
2. \(\text{Slide} \; n \): Removes the top \(n\) elements __after the first element on the stack__.
2020-03-06 21:28:09 -08:00
The first element is not removed.
2. \(\text{Offset} \; n \): Pushes an element from the stack onto the stack, again. When \(n=0\),
the top element is pushed, when \(n=1\), the second element is pushed, and so on.
3. \(\text{Eq}\): Compares two numbers on top of the stack for equality. The numbers are removed,
2020-03-06 21:28:09 -08:00
and replaced with a boolean indicating whether or not they are equal.
4. \(\text{PushI} \; i \): Pushes an integer \(i\) onto the stack.
5. \(\text{Add}\): Adds two numbers on top of the stack. The two numbers are removed,
2020-03-06 21:28:09 -08:00
and replaced with their sum.
6. \(\text{Mul}\): Multiplies two numbers on top of the stack. The two numbers are removed,
2020-03-06 21:28:09 -08:00
and replaced with their product.
7. \(\textbf{if}\)/\(\textbf{else}\): Runs the first list of commands if the boolean "true" is
2020-03-06 21:28:09 -08:00
on top of the stack, and the second list of commands if the boolean is "false".
8. \(\textbf{func}\): pushes a function with the given commands onto the stack.
9. \(\text{Call}\): calls the function at the top of the stack. The function is removed,
2020-03-06 21:28:09 -08:00
and its body is then executed.
Great! Let's now write some dummy programs in our language (and switch to code blocks
from LaTeX). How about a program that multiplies 4 and 5?
```
PushI 5
PushI 4
Mul
```
Next, let's try something more complicated.
{{< sidenote "right" "contrived-note" "How about a program that checks if 3 is equal to 4, and returns 999 if they are equal, and 1 if they are not?" >}}
I'm aware that this example is contrived. To minimize the cognitive load of working with our language, I've stripped it of many useful features, including
inequalities. This is why the example may seem strange: I had to pose a question I could answer!
{{< /sidenote >}}
```
PushI 4
PushI 3
Eq
if { PushI 999 } else { PushI 1 }
```
Now, it's time for the actual meat: can our language do recursion?
I claim that it does, but before we start hacking away, there's one more thing we need to do:
establish a calling convention.
### Be Conventional!
Our language does not enforce any etiquette. You can easily create a function
that pops every value off the stack, continuing until the stack is empty.
You can equally easily make a function that fills your stack with random junk.
With such potential for disorder, a programmer --- maybe yourself --- may experience some
{{< sidenote "right" "anomie-note" "anomie." >}}
Anomie is defined as "lack of the usual social or ethical standards in an individual or group" according
to the Oxford dictionary.
{{< /sidenote >}} To deal with this, we try to maintain a little bit of order in the midst
of all the computational chaos. We will adopt calling conventions.
When I say calling convention, I mean that every time we call a function, we do it in a
methodical way. There are many possible such methods, but I propose the following:
1. Since \(\text{Call}\) requires that the function you're calling is at the top
2020-03-06 21:28:09 -08:00
of the stack, we stick with that.
2. If the function expects arguments, we push them on the stack right before the function. The
first argument of the function should be second from the top of the stack (i.e.,
{{< sidenote "right" "offset-note" "accessible from the function via \(\text{Offset} \; 0\))." >}}
Note that \(\text{Call}\) removes the function from the stack, which is why the first argument
ends up at the very top.
{{< /sidenote >}} The second argument should follow, then the third, and so on.
3. When a function returns, it should not leave its arguments on the stack. Instead of them,
the function should leave its resulting value.
4. A function does not modify the stack below the arguments it receives.
Let's try this out with a basic function definition and call. How about a function that
always returns 0, no matter what argument you give it? The function itself
would look something like this:
```
PushI 0
Slide 1
```
Here's how things will play out. When the function is called --- and we assume
that it is called correctly, of course -- it will receive an integer
on top of the stack. That may not, and likely will not, be the only thing on the stack.
However, to stick by convention 4, we pretend that the stack is empty, and that
trying to manipulate it will result in an error. So, we can start by imagining
an empty stack, with an integer \(x\) on top:
2020-03-06 21:28:09 -08:00
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\(x\){{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Then, \(\text{PushI} \; 0\) will push 0 onto the stack:
2020-03-06 21:28:09 -08:00
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}0{{< /stack_element >}}
{{< stack_element >}}\(x\){{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
\(\text{Slide} \; 1\) will then remove the 1 element after the top element: \(x\).
2020-03-06 21:28:09 -08:00
We end up with the following stack:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}0{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
The function has finished running, and we maintain convention 3: the function's
return value is in place of its argument on the stack.
All that's left is to call this function. Let's try calling the function
with the number 15. We do this like so:
```
PushI 15
func { PushI 0; Slide 1 }
Call
```
The function must be on top of the stack, as per the semantics of our language
(and, I suppose, convention 1). Because of this, we have to push it last.
It only takes one argument, which we push on the stack first (so that it ends up
below the function, as per convention 2). When both are pushed, we use
\(\text{Call}\) to execute the function, which will proceed as we've seen above.
2020-03-06 21:28:09 -08:00
### Get Ahold of Yourself!
How should a function call itself? The fact that functions reside on the stack,
and can therefore be manipulated in the same way as any stack elements. This
opens up an opportunity for us: we can pass the function as an argument
to itself! Then, when it needs to make a recursive call, all it must do
is \(\text{Offset}\) itself onto the top of the stack, then \(\text{Call}\),
2020-03-06 21:28:09 -08:00
and voila!
Talk is great, of course, but talking doesn't give us any examples. Let's
walk through an example of writing a recursive function this way. Let's
try [factorial](https://en.wikipedia.org/wiki/Factorial)!
The "easy" implementation of factorial is split into two cases:
the base case, when \(0! = 1\) is computed, and the recursive case,
in which we multiply the input number \(n\) by the result
of computing factorial for \(n-1\). Accordingly, we will use
the \(\textbf{if}\)/\(\text{else}\) command. We will
2020-03-06 21:28:09 -08:00
make our function take two arguments, with the number input
as the first ("top") argument, and the function itself as
the second argument. Importantly, we do not want to destroy the input
number by running \(\text{Eq}\) directly on it. Instead,
we first copy it using \(\text{Offset} \; 0\), then
2020-03-06 21:28:09 -08:00
compare it to 0:
```
Offset 0
PushI 0
Eq
```
Let's walk through this. We start with only the arguments
on the stack:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Then, \(\text{Offset} \; 0\) duplicates the first argument
2020-03-06 21:28:09 -08:00
(the number):
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Next, 0 is pushed onto the stack:
{{< stack >}}
{{< stack_element >}}0{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Finally, \(\text{Eq}\) performs the equality check:
2020-03-06 21:28:09 -08:00
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}true/false{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Great! Now, it's time to branch. What happens if "true" is on top of
the stack? In that case, we no longer need any more information.
We always return 1 in this case. So, just like the function I described
earlier, we can do the following:
```
PushI 1
Slide 2
```
As before, we push the desired answer onto the stack:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}1{{< /stack_element >}}
2020-03-08 00:38:05 -08:00
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Then, to follow convention 3, we must get rid of the arguments. We do this by using \(\text{Slide}\):
2020-03-06 21:28:09 -08:00
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}1{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Great! The \(\textbf{if}\) branch is now done, and we're left with the correct answer on the stack.
2020-03-06 21:28:09 -08:00
Excellent!
It's the recursive case that's more interesting. To make the recursive call, we must carefully
set up our stack. Just like before, the function must be an argument to itself, and it's found
lower on the stack, so we push it first:
```
Offset 1
```
The result is as follows:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Next, we must compute \(n-1\). This is pretty standard stuff:
2020-03-06 21:28:09 -08:00
```
Offset 1
PushI -1
Add
```
Why these three instructions? Well, with the function now on the top of the stack, the number argument is somewhat
buried, and thus, we need to use \(\text{Offset} \; 1\) to get to it:
2020-03-06 21:28:09 -08:00
{{< stack >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Then, we push a negative number, and add it to to the number on top. We end up with:
{{< stack >}}
{{< stack_element >}}\(n-1\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Finally, we have our arguments in order as per convention 2. To follow convention 1, we must
now push the function onto the top of the stack:
```
Offset 1
```
The stack is now as follows:
{{< stack >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< stack_element >}}\(n-1\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
Good! With the preparations for the function call now complete, we take
the leap:
```
Call
```
If the function behaves as promised, this will remove the top 3 elements
from the stack. The top element, which is the function itself, will
be removed by the \(\text{Call}\) operator. The two next two elements
2020-03-06 21:28:09 -08:00
will be removed from the stack and replaced with the result of the function
as per convention 2. The rest of the stack will remain untouched as
per convention 4. We thus expect the stack to look as follows:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\((n-1)!\){{< /stack_element >}}
{{< stack_element >}}\(n\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
We're almost there! What's left is to perform the multiplication (we're
safe to destroy the argument now, since we will not be needing it after
this), and clean up the stack:
```
Mul
Slide 1
```
The multiplication leaves us with \(n(n-1)! = n!\) on top of the stack,
2020-03-06 21:28:09 -08:00
and the function argument below it:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\(n!\){{< /stack_element >}}
{{< stack_element >}}factorial{{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
We then use \(\text{Slide}\) so that only the factorial is on the
2020-03-06 21:28:09 -08:00
stack, satisfying convention 3:
{{< stack >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}{{< /stack_element >}}
{{< stack_element >}}\(n!\){{< /stack_element >}}
{{< /stack >}}
2020-03-06 21:28:09 -08:00
That's it! We have successfully executed the recursive case. The whole
function is now as follows:
```
Offset 0
PushI 0
Eq
if {
PushI 1
Slide 2
} else {
Offset 1
Offset 1
PushI -1
Add
Offset 1
Call
Mul
Slide 1
2020-03-06 21:28:09 -08:00
}
```
We can now invoke this function to compute \(5!\) as follows:
2020-03-06 21:28:09 -08:00
```
func { ... }
PushI 5
Offset 1
Call
```
Awesome! That's about it. We have made a stack-based language with full
support for recursion and procedures. I hope this was helpful.