Worked example: A stack based calculator

In this post, we'll implement a simple stack based calculator (also known as "reverse Polish" style). The implementation is almost entirely done with functions, with only one special type and no pattern matching at all, so it is a great testing ground for the concepts introduced in this series.

If you are not familiar with a stack based calculator, it works as follows: numbers are pushed on a stack, and operations such as addition and multiplication pop numbers off the stack and push the result back on.

Here is a diagram showing a simple calculation using a stack:

The first steps to designing a system like this is to think about how it would be used. Following a Forth like syntax, we will give each action a label, so that the example above might want to be written something like:

EMPTY ONE THREE ADD TWO MUL SHOW

We might not be able to get this exact syntax, but letís see how close we can get.

The Stack data type

First we need to define the data structure for a stack. To keep things simple, weíll just use a list of floats.

First, note that the list structure is immutable, so the function must accept an existing stack and return a new stack. It cannot just alter the existing stack. In fact, all of the functions in this example will have a similar format like this:

Input: a Stack plus other parameters
Output: a new Stack

Next, what should the order of the parameters be? Should the stack parameter come first or last? If you remember the discussion of designing functions for partial application, you will remember that the most changeable thing should come last. Youíll see shortly that this guideline will be born out.

Finally, the function can be made more concise by using pattern matching in the function parameter itself, rather than using a let in the body of the function.

Here is the rewritten version:

letpushx(StackContentscontents)=StackContents(x::contents)

Much nicer!

And by the way, look at the nice signature it has:

valpush:float->Stack->Stack

As we know from a previous post, the signature tells you a lot about the function.
In this case, I could probably guess what it did from the signature alone, even without knowing that the name of the function was "push".
This is one of the reasons why it is a good idea to have explicit type names. If the stack type had just been a list of floats, it wouldn't have been as self-documenting.

Building on top of "push"

With this simple function in place, we can easily define an operation that pushes a particular number onto the stack.

letONEstack=push1.0stackletTWOstack=push2.0stack

But wait a minute! Can you see that the stack parameter is used on both sides? In fact, we donít need to mention it at all. Instead we can skip the stack parameter and write the functions using partial application as follows:

Popping the stack

That takes care of pushing onto the stack ó what about a pop function next?

When we pop the stack, we will return the top of the stack, obviously, but is that all?

In an object-oriented style, the answer is yes. In an OO approach, we would mutate the stack itself behind the scenes, so that the top element was removed.

But in a functional style, the stack is immutable. The only way to remove the top element is to create a new stack with the element removed.
In order for the caller to have access to this new diminished stack, it needs to be returned along with the top element itself.

In other words, the pop function will have to return two values, the top plus the new stack. The easiest way to do this in F# is just to use a tuple.

Here's the implementation:

/// Pop a value from the stack and return it /// and the new stack as a tupleletpop(StackContentscontents)=matchcontentswith|top::rest->letnewStack=StackContentsrest(top,newStack)

This function is also very straightforward.

As before, we are extracting the contents directly in the parameter.

We then use a match..with expression to test the contents.

Next, we separate the top element from the rest, create a new stack from the remaining elements and finally return the pair as a tuple.

Try the code above and see what happens. You will get a compiler error!
The compiler has caught a case we have overlooked -- what happens if the stack is empty?

Generally, I prefer to use error cases, but in this case, we'll use an exception. So here's the pop code changed to handle the empty case:

/// Pop a value from the stack and return it /// and the new stack as a tupleletpop(StackContentscontents)=matchcontentswith|top::rest->letnewStack=StackContentsrest(top,newStack)|[]->failwith"Stack underflow"

Writing the math functions

Now with both push and pop in place, we can work on the "add" and "multiply" functions:

letADDstack=letx,s=popstack//pop the top of the stacklety,s2=pops//pop the result stackletresult=x+y//do the mathpushresults2//push back on the doubly-popped stackletMULstack=letx,s=popstack//pop the top of the stacklety,s2=pops//pop the result stackletresult=x*y//do the math pushresults2//push back on the doubly-popped stack

Time to refactor...

It is obvious that there is significant duplicate code between these two functions. How can we refactor?

Both functions pop two values from the stack, apply some sort of binary function, and then push the result back on the stack. This leads us to refactor out the common code into a "binary" function that takes a two parameter math function as a parameter:

letbinarymathFnstack=// pop the top of the stacklety,stack'=popstack// pop the top of the stack againletx,stack''=popstack'// do the mathletz=mathFnxy// push the result value back on the doubly-popped stackpushzstack''

Note that in this implementation, I've switched to using ticks to represent changed states of the "same" object, rather than numeric suffixes. Numeric suffixes can easily get quite confusing.

Question: why are the parameters in the order they are, instead of mathFn being after stack?

Now that we have binary, we can define ADD and friends more simply:

Here's a first attempt at ADD using the new binary helper:

letADDaStack=binary(funxy->x+y)aStack

But we can eliminate the lambda, as it is exactly the definition of the built-in + function! Which gives us:

letADDaStack=binary(+)aStack

And again, we can use partial application to hide the stack parameter. Here's the final definition:

In a similar fashion, we can create a helper function for unary functions

letunaryfstack=letx,stack'=popstack//pop the top of the stackpush(fx)stack'//push the function value on the stack

And then define some unary functions:

letNEG=unary(funx->-x)letSQUARE=unary(funx->x*x)

Test interactively again:

letneg3=EMPTY|>THREE|>NEGletsquare2=EMPTY|>TWO|>SQUARE

Putting it all together

In the original requirements, we mentioned that we wanted to be able to show the results, so letís define a SHOW function.

letSHOWstack=letx,_=popstackprintfn"The answer is %f"xstack// keep going with same stack

Note that in this case, we pop the original stack but ignore the diminished version. The final result of the function is the original stack, as if it had never been popped.

So now finally, we can write the code example from the original requirements

EMPTY|>ONE|>THREE|>ADD|>TWO|>MUL|>SHOW

Going further

This is fun -- what else can we do?

Well, we can define a few more core helper functions:

/// Duplicate the top value on the stackletDUPstack=// get the top of the stackletx,_=popstack// push it onto the stack againpushxstack/// Swap the top two valuesletSWAPstack=letx,s=popstacklety,s'=popspushy(pushxs')/// Make an obvious starting pointletSTART=EMPTY

And with these additional functions in place, we can write some nice examples:

In each of these cases, a new function is defined by composing other functions together to make a new one. This is a good example of the "combinator" approach to building up functionality.

Pipes vs composition

We have now seen two different ways that this stack based model can be used; by piping or by composition. So what is the difference? And why would we prefer one way over another?

The difference is that piping is, in a sense, a "realtime transformation" operation. When you use piping you are actually doing the operations right now, passing a particular stack around.

On the other hand, composition is a kind of "plan" for what you want to do, building an overall function from a set of parts, but not actually running it yet.

So for example, I can create a "plan" for how to square a number by combining smaller operations:

letCOMPOSED_SQUARE=DUP>>MUL

I cannot do the equivalent with the piping approach.

letPIPED_SQUARE=DUP|>MUL

This causes a compilation error. I have to have some sort of concrete stack instance to make it work:

letstackWith2=EMPTY|>TWOlettwoSquared=stackWith2|>DUP|>MUL

And even then, I only get the answer for this particular input, not a plan for all possible inputs, as in the COMPOSED_SQUARE example.

The other way to create a "plan" is to explicitly pass in a lambda to a more primitive function, as we saw near the beginning:

letLAMBDA_SQUARE=unary(funx->x*x)

This is much more explicit (and is likely to be faster) but loses all the benefits and clarity of the composition approach.

So, in general, go for the composition approach if you can!

The complete code

Here's the complete code for all the examples so far.

// ==============================================// Types// ==============================================typeStack=StackContentsoffloatlist// ==============================================// Stack primitives// ==============================================/// Push a value on the stackletpushx(StackContentscontents)=StackContents(x::contents)/// Pop a value from the stack and return it /// and the new stack as a tupleletpop(StackContentscontents)=matchcontentswith|top::rest->letnewStack=StackContentsrest(top,newStack)|[]->failwith"Stack underflow"// ==============================================// Operator core// ==============================================// pop the top two elements// do a binary operation on them// push the result letbinarymathFnstack=lety,stack'=popstackletx,stack''=popstack'letz=mathFnxypushzstack''// pop the top element// do a unary operation on it// push the result letunaryfstack=letx,stack'=popstackpush(fx)stack'// ==============================================// Other core // ==============================================/// Pop and show the top value on the stackletSHOWstack=letx,_=popstackprintfn"The answer is %f"xstack// keep going with same stack/// Duplicate the top value on the stackletDUPstack=letx,s=popstackpushx(pushxs)/// Swap the top two valuesletSWAPstack=letx,s=popstacklety,s'=popspushy(pushxs')/// Drop the top value on the stackletDROPstack=let_,s=popstack//pop the top of the stacks//return the rest// ==============================================// Words based on primitives// ==============================================// Constants// -------------------------------letEMPTY=StackContents[]letSTART=EMPTY// Numbers// -------------------------------letONE=push1.0letTWO=push2.0letTHREE=push3.0letFOUR=push4.0letFIVE=push5.0// Math functions// -------------------------------letADD=binary(+)letSUB=binary(-)letMUL=binary(*)letDIV=binary(/)letNEG=unary(funx->-x)// ==============================================// Words based on composition// ==============================================letSQUARE=DUP>>MULletCUBE=DUP>>DUP>>MUL>>MULletSUM_NUMBERS_UPTO=DUP// n >>ONE>>ADD// n+1>>MUL// n(n+1)>>TWO>>SWAP>>DIV// n(n+1) / 2

Summary

So there we have it, a simple stack based calculator. We've seen how we can start with a few primitive operations (push, pop, binary, unary) and from them, build up a whole domain specific language that is both easy to implement and easy to use.

As you might guess, this example is based heavily on the Forth language. I highly recommend the free book "Thinking Forth", which is not just about the Forth language, but about (non object-oriented!) problem decomposition techniques which are equally applicable to functional programming.

I got the idea for this post from a great blog by Ashley Feniello. If you want to go deeper into emulating a stack based language in F#, start there. Have fun!