Previously, in Part I, and continuing into Part II of this tutorial series, we delved into a handful of useful concepts. Some of these are specific to the Unity engine, such as animating a button press with coroutines or working with Unity events and utilizing TextMesh Pro. On the other hand, concepts like building a tokenizer, a recursive descent parser, or an Abstract Syntax Tree (commonly abbreviated as AST) are fundamental and can be found in many applications.
In Part III, we adapted this project to a VR environment, exploring the integration of OpenXR, XR Interaction Toolkit, and XR Hands packages. Note that this final part is specific to VR in Unity and is not directly related to the content in this addendum.
However, if you haven't read the first two parts, I recommend doing so. Today, we're starting with the project as it was at the end of Part II, and you can download it from GitHub.
The goal is to transform our classic single-line calculator into an RPN calculator, which, as the name suggests, is based on Reverse Polish Notation, also known as postfix notation.
Let's consider a simple expression: 2 + 1
. While it's so common that we usually don't specify it's written using infix notation, the same expression can be represented as + 2 1
in prefix notation (or Polish notation) or 2 1 +
in postfix notation (or RPN), which we'll be focusing on today.
How an RPN calculator works
You'll see that the implementation of an RPN calculator is significantly simpler than the one with a tokenizer, parser, and AST for evaluating expressions in infix notation. With RPN, there's no need for parentheses, and we don't have to worry about the order of operations since it can be evaluated in a linear fashion, from left to right
For example, the infix expression (1 * 2) - (3 * 4)
would become 1 2 * 3 4 * +
in RPN. To evaluate the expression programmatically, we just need basic math operations and a stack. Since this is an addendum to a tutorial series where we've covered advanced topics, I'll assume you know what a stack is and how it works.
Let's do it by hand before we start coding. We push 1
and then 2
to the stack, so our stack looks like this.
: 2 1 ]
Now, we push an *
operator. However, the operator itself is not pushed onto the stack. Instead, two elements are popped from the stack, multiplied, and the result is then pushed back onto the stack.
1 * 2 = 2
; we push the result back to the stack, and since the two numbers were popped, our stack now contains only the result.
: 2 ]
Let's push 3
and 4
, and now the stack has three items.
: 4 3 2 ]
After pushing *
, we once again pop the two topmost elements and multiply them. The result of 3 * 4 = 12
, so we push 12
back onto the stack.
: 12, 2 ]
The last token is -
, and I chose subtraction on purpose. With addition or multiplication, in other words, with commutative operations, it's not so obvious that we have to always put the first element from the stack on the right side and the second on the left side. In this case, 2 - 12 = -10
, and our stack is now empty. We push the result -10
back onto the stack for further operations.
: -10 ]
We can confirm that we get the same result using the more common approach with infix notation. When evaluating (1 * 2) - (3 * 4)
, first, we evaluate the sub-expressions in parentheses, and then 2 - 12
is indeed -10
.
Stack Decorator Class
Before we start re-implementing the "brain" of our calculator, let's create a simple decorator for the Stack<T> class. This decorator will allow us to observe in the Unity Editor console what enters and exits the stack, as well as the numbers currently stored in the stack.
In the RPN class, we'll be using StackConsoleLogged<double>
instead of Stack<double>
. The beauty of this simple decorator is that we don't have to worry about logging anymore.
As long as we access the stack via this decorator class, we consistently get debug log messages every time we pop something from or push something onto the stack. When logging is no longer required, we can simply change StackConsoleLogged
to a regular Stack
at just one place.
RPN Class
We're going to encapsulate the core of the implementation in a class. First, let's create the new class named RPN
and add a stack
member variable.
Then, add a Dictionary<string, Func<double, double, double>>
to associate operators +
, -
, *
, /
, and ^
with functions that accept two parameters of type double
and return a double
. Thanks to lambda expressions, we can create a very neat block of code.
A Dictionary like this is often a cleaner alternative to if and switch statements or even switch expressions, which is a relatively new feature in C#. Now, we just need to add two public methods, Push and Pop. Let's start with the simpler one.
This makes our stack virtually infinite. After pushing numbers 1
, 2
, and 3
, we can imagine it like this.
: 3 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -> ∞
Though, in memory, of course, the stack would really contain just three elements: [ 1, 2, 3 ]
. After implementing the Push
method, our RPN class will be complete.
We first try to parse the input using double.TryParse
method. If parsing succeeds, it means the input is a number. In that case, we push it to the internal stack and return early from the method.
When parsing fails, the method returns false, indicating that the input is an operator. Technically, we could pass something else, and then we'd get an exception at line 12 since our operators dictionary won't have such a key. However, in the way we'll be using this class, we can assume the input would be either a number or a known operator.
Now that we know the input is an operator, we pop two elements from the internal stack using our Pop
method to get 0.0
if the stack is empty, instead of throwing an InvalidOperationException
, and store them in variables a
and b
.
Then, we access a function associated with the operator from the operators dictionary and invoke the function with arguments b and a. Notice the order of arguments here: first is b
, the second is a
.
If the result is valid, meaning it is not NaN
(not a number), we push the result back onto the stack. This method essentially replicates what we've been doing manually in the 'How an RPN calculator works' section.
Changes in Display Component
Now that we've implemented our RPN
class, we just need to make a few changes in the Display
class. Remove the member variables inFaultState
and lastOpIsEvaluation
from the top and add typeOver
, pushOnType
, and rpn
instead.
In the Awake method, we create a new instance of the RPN class and assign it to the rpn member variable.
Then, we need to change the implementation of the Type
method. We no longer need to call IsNumberOrBracket
method, and when typeOver
is true
, before clearing the display, we want to push its content to the stack on the RPN instance.
Pushing the result of the previous operation back to the stack is important because, to see the result, we're going to pop it instead of just peeking the stack. This allows us to chain operations conveniently, instead of always have to manually pushing the result back by pressing the Enter button, as you'll soon see.
We also no longer need to check if the argument is a number or a bracket. We are only interested in clearing the content of textMesh
before appending a new character if the current content is "0"
. You can remove the entire private bool IsNumberOrBracket(char c)
method.
The EraseOne
method needs just a small change. Replace inFaultState || lastOpIsEvaluation
in the condition with just typeOver
.
public void EraseOne()
{
- if (inFaultState || lastOpIsEvaluation || textMesh.text.Length == 1)
+ if (typeOver || textMesh.text.Length == 1)
{
Clear();
return;
}
textMesh.text = textMesh.text.Remove(textMesh.text.Length - 1);
}
Finally, remove the entire Evaluate
method, as we're going to implement Operate
and Enter
methods instead. Let's start with the simpler one.
public void Enter()
{
rpn.Push(textMesh.text);
typeOver = true;
pushOnType = false;
}
When we press the Enter key on our calculator (which we're later going to replace the Equals key with), we just want to push the content of the display to the stack and set flags typeOver
to true
and pushOnType
to false
. This ensures that when we start typing another number, the current content of the display is erased before the new content is pushed to the stack.
The Operate method will be called when we press one of the operator keys on our calculator.
First, we push the content of the display to the stack, and then we also push the operator, triggering the operation to be performed on the topmost two elements, as you already know.
Then, we pop the result from the stack to display it, and that's why we want to push the result back to the stack when we start typing another number. After the operation is performed, we also always want to clear the display and start typing over.
When, instead of typing a new number, we press another operator—like when evaluating an expression 1 2 * 3 4 * +
, for example—we call the Operate method again. This method always pushes the content of the display first to the stack. Without popping the result, it would be in the stack twice.
You can think of the display as virtually being a part of the stack in this implementation—it always represents the head, or the topmost element.
Changes in Calc.prefab
Changing methods in the Display
class broke our On Press
events on Key
components on buttons in Calc.prefab
.
Before we fix them, let's first make some other small changes. Open the Calc.prefab
and remove the CalcButton_LeftBracket
and CalcButton_RightBracket
game objects; we're not going to need them, as we've already discussed that the RPN calculator has no need for brackets.
Next, rename CalcButton_Eval
to CalcButton_Enter
. We're going to repurpose this button. In our RPN calculator, the Enter button pushes the number to the stack using the Enter
method in our Display
class.
Since we no longer have brackets and we changed the equals button to the enter button, let's also replace the image at Assets/Calc/Textures/CalcButtons.png
with the following updated version.
Finally, select game objects for all buttons with operators—CalcButton_Div
, CalcButton_Mul
, CalcButton_Sub
, CalcButton_Add
, CalcButton_Pow
—and in the Inspector, fix the broken event On Press
event handlers to Display.Operate
.
Now select CalcButton_Enter
and fix the event similarly, but this time set the event handler to Display.Enter
.
That concludes all the necessary changes; our RPN calculator should now be fully working. For a simple exercise, consider putting back one of the bracket buttons and repurposing it to a Pop button, which should does the opposite of the Enter button.
Final Thoughts
As you just saw, implementing an RPN calculator is much more straightforward than implementing a classic single-line calculator, where we need to tokenize and parse the entire input to create an AST before evaluation. In general, every problem usually has multiple solutions, and each solution comes with its own set of pros and cons.
I hope you enjoyed this addendum and learned something new. The Unity project with the complete implementation is available for you on GitHub, in the original repository for Part II, in a separate branch named rpn.