Start on the recursion tutorial post
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
d8d1aa66e6
commit
f202c8ea44
342
content/blog/stack_recursion.md
Normal file
342
content/blog/stack_recursion.md
Normal file
|
@ -0,0 +1,342 @@
|
|||
---
|
||||
title: Creating Recursive Functions in a Stack Based Language
|
||||
date: 2020-03-06T17:56:55-08:00
|
||||
tags: ["Programming Languages", "Haskell"]
|
||||
draft: true
|
||||
---
|
||||
|
||||
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__.
|
||||
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,
|
||||
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,
|
||||
and replaced with their sum.
|
||||
6. \\(\\text{Mul}\\): Multiplies two numbers on top of the stack. The two numbers are removed,
|
||||
and replaced with their product.
|
||||
7. \\(\\textbf{if}\\)/\\(\\textbf{else}\\): Runs the first list of commands if the boolean "true" is
|
||||
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,
|
||||
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
|
||||
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:
|
||||
|
||||
{{< todo >}}Stack with x on top{{< /todo >}}
|
||||
|
||||
Then, \\(\\text{PushI} \\; 0\\) will push 0 onto the stack:
|
||||
|
||||
{{< todo >}}Stack with x then 0{{< /todo >}}
|
||||
|
||||
\\(\\text{Slide} \\; 1\\) will then remove the 1 element after the top element: \\(x\\).
|
||||
We end up with the following stack:
|
||||
|
||||
{{< todo >}}Stack with 0{{< /todo >}}
|
||||
|
||||
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.
|
||||
|
||||
### 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}\\),
|
||||
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
|
||||
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
|
||||
compare it to 0:
|
||||
|
||||
```
|
||||
Offset 0
|
||||
PushI 0
|
||||
Eq
|
||||
```
|
||||
|
||||
Let's walk through this. We start with only the arguments
|
||||
on the stack:
|
||||
|
||||
{{< todo >}}image of stack of factorial call{{< /todo >}}
|
||||
|
||||
Then, \\(\\text{Offset} \\; 0\\) duplicates the first argument
|
||||
(the number):
|
||||
|
||||
{{< todo >}}image of stack of factorial call with number duped{{< /todo >}}
|
||||
|
||||
Next, 0 is pushed onto the stack:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with number duped, and zero{{< /todo >}}
|
||||
|
||||
Finally, \\(\\text{Eq}\\) performs the equality check:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with boolean{{< /todo >}}
|
||||
|
||||
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:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with 1 on the stack{{< /todo >}}
|
||||
|
||||
Then, to follow convention 3, we must get rid of the arguments. We do this by using \\(\\text{Slide}\\):
|
||||
|
||||
{{< todo >}}image of stack of factorial call with only 1 on the stack{{< /todo >}}
|
||||
|
||||
Great! The \\(\\textbf{if}\\) branch is now done, and we're left with the correct answer on the stack.
|
||||
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:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with extra function on top{{< /todo >}}
|
||||
|
||||
Next, we must compute \\(n-1\\). This is pretty standard stuff:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with extra function and number on top{{< /todo >}}
|
||||
|
||||
Then, we push a negative number, and add it to to the number on top. We end up with:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with extra function and number-1 on top{{< /todo >}}
|
||||
|
||||
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:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with extra function and number-1
|
||||
and extra function on top{{< /todo >}}
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with with (n-1)! on top{{< /todo >}}
|
||||
|
||||
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,
|
||||
and the function argument below it:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with with n! on top{{< /todo >}}
|
||||
|
||||
We then use \\(\\text{Slide}\\) so that only the factorial is on the
|
||||
stack, satisfying convention 3:
|
||||
|
||||
{{< todo >}}image of stack of factorial call with with n! on top{{< /todo >}}
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
We can now invoke this function to compute \\(5!\\) as follows:
|
||||
|
||||
```
|
||||
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.
|
Loading…
Reference in New Issue
Block a user