Beginnings with Rust

I started learning Rust last month during some unusual Covid-19-and-trapped-indoors-due-to-wildfire-smoke free time, and I've had fun with it. For some cross-platform hobby projects of mine I’ve done some experimentation with self-contained app bundles using Java, and after some struggles I still haven't got it to work nicely. I haven't specifically looked at app bundling with Rust yet, but given that it compiles to a native binary it should be easier to get MacOS apps and Linux appimages working. I’d heard and read a lot about Rust's reputation, so it was an easy choice for a new language to play with.

I started by reading a large portion of Rust's web-based "the book" and working through some of the described exercises. The first thing I noticed of course was the compiler. It's a breath of fresh air compared to what I'm used to with Java, Python, and JavaScript. Coming from those very high-level languages, the compiler seems much more restrictive but also way more helpful in its error messages. By being both strict, with respect to memory safety, and helpful, it seems like writing better code is unavoidable.

cargo has been very nice to use, as a beginner. Again, it's a breath of fresh air coming from Java and Maven. Nothing that hasn't been said before, of course.

My app: a calculator

For my first stab at a Rust project I tried to write something that would actually be useful for my day-to-day life. For a while now I've been wanting to find a simple calculator app, for MacOS, that allows typing out and evaluating basic math expressions while showing a history of recent calculations. Rather than look around and buy one, it seemed perfect for my beginner Rust project.

I initially looked for cross-platform Rust GUI libraries (apparently the lack of a mature cross-platform GUI library is a known issue) and tried to use iced, which was relatively easy to get it up and running. It being in the relatively early stages of development, without mature documentation nor a set of standard widgets that includes mouse-selectable text, I found it wasn't quite up to the task yet. So I shelved the idea of writing a GUI for this app.

I chose termion to make a terminal-based UI. It worked great and allowed me to quickly make my app UI. As a bonus, the MacOS Terminal utility allows making custom terminal profiles that can run a custom command upon opening, so I can easily launch my app in a new terminal window with a large font size and custom color scheme, which gets me close enough to the GUI experience I originally envisioned.

For evaluating math expressions, the eval crate did the job very easily*. But because my goal for the app was to use it for everyday financial calculations (I am a major spreadsheet user for my personal finances) I couldn’t stand the use of floating point math within the eval crate. I needed to implement math using a library similar to Java’s BigDecimal.

Floating-point math... !

*Note: the eval crate is no longer being maintained. See below for an update on replacing it.

The aptly-named bigdecimal crate fit the bill perfectly. In order to put it into use I decided to write my own math expression evaluation engine. In school I’d studied math expression evaluation using a tree, so that’s where I started. Aside from that experience I am a complete novice to this sort of thing, so I thought it would be fun to try to write up an expression evaluator without looking at some existing solution.

I came across https://sachanganesh.com/programming/graph-tree-traversals-in-rust/ that walks though the process of building a tree and performing a pre-order traversal on it, but without the use of recursion (which complicates things a bit I'm guessing). Not having seen any mention of recursion in Rust’s "the book" I didn’t want to go there. An iterative method for evaluating math expressions seemed like a cool project.

Writing the evaluation tree

First, I needed to split a given calculation string (123 + 456) into tokens (123, +, 456). The parsing is a bit odd in places to handle negative numbers (5--.1 becomes 5, -, -.1), but is otherwise straightforward.

I adapted some of the ideas from the blog post, mostly importantly the concept of a separate vector for storing the nodes themselves, where the tree nodes store only an index into that vector, which the blog author called an "arena allocator". Implementing a post-order iterator wasn’t so hard, aside from some fiddling with lifetimes and generics. At that point I could manually build an arbitrary tree and perform a post-order traversal.

Next, I needed to build a valid expression tree from a given calculation, including handling parentheses and order of operations. This took a couple tries, but I ended up with a solution that works for me. I ended up needing to add knowledge of "parent" for each node in the tree to allow backwards traversal while handling some tokens, like the + and - operators. In hindsight it may have been easier to perform a simple search through the tree to implement the get_node_parent function rather than having to handle keeping each node's parent references up-to-date when adding nodes to the tree.

An example tree:

Putting it all together

Evaluating an expression once I had it arranged in a valid tree was very straightforward, even when using bigdecimal. With a simple stack and iterating through the post-order traversal, the final expression result is easily calculated.

Since I am not 100% confident in my expression tree construction code, I left in the eval crate and, for each entered calculation, both the bigdecimal tree-based and the eval-based evaluations are performed. The results are expected to either be the same or, due to floating point math, almost exactly the same. If the results are too far apart (>=0.001%) I display a warning and the results of both calculations. Perhaps some day I’ll type in a calculation that won’t be handled properly, giving me a chance to improve my tree-building code.

Overall I am liking Rust quite a bit, its build system, compiler, etc. rust-analyzer with Sublime Text 3 is pretty nice, but not as ideal as an IntelliJ IDEA or the Eclipse IDE for Java, obviously. Rust is totally usable for me though, for my hobby projects.

Update on the eval crate

I didn't look closely enough initially to see that the eval crate had been abandoned by its author two years ago. I switched to using a more active fork of that project, the evalexpr crate.

Unfortunately, when an expression uses only integers, like 5/3, I couldn't get evalexpr to not truncate the decimal portion of the result into an integer. So instead of 5/3 = 1.666666... it would return 5/3 = 1! My workaround for this was a hack — replace any integer value in the expression with a decimal equivalent. When 5/3 is entered into the calculator, what is actually given to evalexpr to be evaluated is 5.0/3.0, which returns the expected result.

This project is on GitHub.