Consistent with the principles of functional programming, our goal is to remove the state and mutation from our program. But first, we need to reconsider more than just the implementation of our stack.
Recall that stacker was designed around the idea of wrapping each argument in the source code within its own (handle ···) datum. So source code that looked like this:
Would end up as a series of S-expressions that looked like this:
1 2 3 4 5 | (handle 4) (handle 8) (handle +) (handle 3) (handle *) |
Sure, this was obvious & convenient. But it also resulted in a language implementation where each call to handle only knew about the current argument. Moreover, each call to handle was independent of the ones that come before and after. So we had no choice but to maintain the program state using an external stack variable, and mutate it as the program progressed. This style of programming—where function calls use mutation to change the program state—is known as imperative programming. It has its place. But it’s not what we’re going for.
If we want to get rid of state & mutation, we need to find a different way to maintain the stack as we handle each argument. And this starts at the top, with the way the expressions in our program are nested—what in Racket is called the shape of the code.
One option would be to nest the calls to handle (but let’s call this theoretical function h2, since it won’t work the same way as handle):
What would happen here? S-expressions are evaluated from the inside out. So (h2 4) would be evaluated first, and then its result used as the first argument in (h2 result 8), and so on. So we’d be processing our arguments in the correct order, and we could pass the interim stack from one expression to the next. At the end, we’d have the finished stack.
Another option would be to pass all the arguments to a single function (let’s call this one handle-args):
In this case, handle-args could access all the arguments. There’d be no need to mutate a variable outside the boundaries of the function. All the housekeeping could happen inside, and then the function would deliver the finished stack as its return value.
Though the nesting patterns are different, it turns out that both approaches end up working mostly the same way. But the h2 approach requires a little more knowledge about macros than we currently have. So we’ll go with the handle-args approach—pass all the arguments to a single function.
Our reader still consists of a single read-syntax function. Let’s replace the existing read-syntax in funstacker.rkt with this code:
1 2 3 4 5 6 7 8 9 | #lang br/quicklang (define (read-syntax path port) (define src-lines (port->lines port)) (define src-datums (format-datums '~a src-lines)) (define module-datum `(module funstacker-mod "funstacker.rkt" (handle-args ,@src-datums))) (datum->syntax #f module-datum)) (provide read-syntax) |
This version works basically the same way as before. But this time, rather than wrap each argument in its own (handle ···) datum, we’ll convert the arguments into datums without wrapping them at all, storing them in a list called src-datums. Then, inside our module-datum, we’ll splice the src-datums into a single (handle-args ···) expression. handle-args will be the name of the new function that will process our arguments.
As before, we need to provide a #%module-begin macro that acts as the starting point for the expander. Let’s replace the existing #%module-begin in funstacker.rkt with this code:
1 2 3 4 | (define-macro (funstacker-module-begin HANDLE-ARGS-EXPR) #'(#%module-begin (display (first HANDLE-ARGS-EXPR)))) (provide (rename-out [funstacker-module-begin #%module-begin])) |
In our new read-syntax, we saw that our module-datum will contain exactly one expression, namely a call to (handle-args ···). So the funstacker-module-begin macro only has to be able to handle this one argument as input, which we name HANDLE-ARGS-EXPR. Then, because HANDLE-ARGS-EXPR will return a finished stack, we use it as the input argument in (display (first HANDLE-ARGS-EXPR)). Notice that it replaces stack, the state variable that we’re eliminating.
Then we need to implement our stack. In funstacker.rkt, we replace stack, pop-stack!, push-stack! and handle with our new handle-args function:
1 2 3 4 5 6 7 8 9 10 11 | (define (handle-args . args) (for/fold ([stack-acc empty]) ([arg (in-list args)] #:unless (void? arg)) (cond [(number? arg) (cons arg stack-acc)] [(or (equal? * arg) (equal? + arg)) (define op-result (arg (first stack-acc) (second stack-acc))) (cons op-result (drop stack-acc 2))]))) (provide handle-args) |
Some of this looks similar to our old handle function. But we’re moving from an imperative style to a functional style, and introducing some new notation. Let’s go through this function line by line.
1 | (define (handle-args . args) |
The dot in front of args designates it as a rest argument. A rest argument is optional in any function definition. It can only appear as the last argument. Any number of positional arguments can appear before it. The rest argument means “gather the remaining arguments in a list and assign it to this variable”. A rest argument makes it possible for a function to accept any number of arguments. That’s what we need here, since we don’t know in advance how many arguments will be in the source code.
Next we have for/fold, which deserves a place in everyone’s list of favorite Racket tools. for/fold iterates over a list of values (like the for loops we’ve seen in other languages). But each pass of the loop also returns a special value called an accumulator. + The accumulator is the destination of the “folding”, and is also used in the related functions foldl and foldr. An accumulator is similar to a state variable, with two differences. First, the accumulator lives inside our loop, rather than outside. Second, on each pass of for/fold, rather than change the value of the accumulator with set!, we replace it with a whole new value. The loop ends when the iterator runs out of values. The return value of the whole for/fold loop is the final value of its accumulator.
Let’s consider a simple for/fold loop just so we understand the mechanics:
Here, our accumulator is sum and starts with the value 0. Our iterator is int, which will take on each value in (list 1 2 3 4 5 6 7 8 9 10). On each pass of the loop, (+ sum int) means that we add the current value of the sum accumulator to the current value of int. This result becomes the new accumulator value for the next pass. So we end up adding the integers together, and the return value of the whole loop is 55.
Going back to our loop:
A for/fold loop has two mandatory parts.
We have to define at least one accumulator and assign it an initial value. We’ll call our accumulator stack-acc, and make it empty to start.
We have to define at least one iterator. We’ll call this arg and iterate over our list of args.
This loop also uses two optional parts:
A guard expression, which limits the iteration with a test condition. In this case, some of the lines in our source program might be blank. Since they serve no purpose, we’d rather ignore them. When these blank lines are converted to datums, they’ll end up as the special Racket value <#void>, which can be tested with void?. So we use an #:unless condition to skip void? values of arg. + For more about guard expressions, see loops.
The sequence constructor in-list. Any sequence can be used directly as a source of iterator values, including any list (because every list is a sequence). But in-list is a hint to the compiler that helps it generate more efficient code. In a list of 10 elements, it won’t add any practical value. But when you know for sure what type of sequence will be iterated, using the sequence constructor is a virtuous habit.
This cond expression looks similar to the one we had in our original handle function. But this time, instead of mutating an external stack variable, each branch will create an updated copy of our stack-acc accumulator. In essence, instead of relying on external push-stack! and pop-stack! functions, we’ll do all the stack processing inside our cond.
If arg is a number, we push it onto the stack with cons. If arg is a stack operator, we calculate op-result using the first and second elements of stack-acc. Unlike pop-stack!, these operations don’t remove arguments from the stack, so we have to explicitly drop two items from stack-acc. Then we put op-result onto the stack (again with cons). The new stack that’s created at the end of each branch will be returned as the value of the whole cond expression, and thereby become the new value of stack-acc used in the next iteration.
Finally, as we did in stacker, we need to provide our stack operators:
Our whole "funstacker.rkt" should now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #lang br/quicklang (define (read-syntax path port) (define src-lines (port->lines port)) (define src-datums (format-datums '~a src-lines)) (define module-datum `(module funstacker-mod "funstacker.rkt" (handle-args ,@src-datums))) (datum->syntax #f module-datum)) (provide read-syntax) (define-macro (funstacker-module-begin HANDLE-ARGS-EXPR) #'(#%module-begin (display (first HANDLE-ARGS-EXPR)))) (provide (rename-out [funstacker-module-begin #%module-begin])) (define (handle-args . args) (for/fold ([stack-acc empty]) ([arg (in-list args)] #:unless (void? arg)) (cond [(number? arg) (cons arg stack-acc)] [(or (equal? * arg) (equal? + arg)) (define op-result (arg (first stack-acc) (second stack-acc))) (cons op-result (drop stack-acc 2))]))) (provide handle-args) (provide + *) |
If we run our "funstacker-test.rkt" file:
We’ll get:
1 | 36
|
Our conversion to functional programming is complete. Better still, funstacker is even shorter than stacker.