Rewriting Beancount in Rust

When I first heard about Rust, “Rewrite It In Rust” was just becoming something of a meme. So what better way to introduce myself to the community than by showing my own experience of rewriting something in Rust.

When first I first heard of the meme, I remember feeling put off Rust, because who wants to use a language whose proponents diminish the sweat, blood, and tears others have put into their projects like this.

After learning Rust, and following the community for a while, I found that most Rustaceans are not raving lunatics, who demand anything and everything be written in Rust. Who would've thought?

Usually, when a Rustacean presents their experience of rewriting something in Rust, they present their motivation and follow solid engineering principles. The projects may be ambitious, granted, but it's a far cry from my earlier imagination of the typical RIIR proponents.

So, in this post, I want to join the ranks of Rustaceans that present alternative tooling in Rust. I want to show you my reimplementation of Beancount in Rust and talk you through my motivation for doing this.

What is Beancount?

But before diving into the technical nitty-gritty, let's take a step back and take a look at the why and what of Beancount.

Some people like to keep track of their finances besides just checking their credit card reports at the end of the month. They record every transaction they make and, for example, can predict how much money they will have at the end of the month.

For such things, you can use a variety of tools. One family of tools, based on Ledger, choose to store their data in plain text files.

This family of plain text accounting tools has some advantages over other approaches. For one, you can read and access your data even without the tool. In many cases, the tools provide little more than reporting on the data you entered manually.

Another advantage, especially relevant for developers, is that you can easily version control your ledger using Git. The plain text accounting wiki lists some more advantages for the curious.

Beancount is one entry in this family of tools, implemented in Python. Beancount follows a philosophy of strictness, assuming that its users are unreliable and likely to make mistakes while entering their data. As such, every transaction has to pass a number of checks before being processed into a report.

Being implemented in Python, Beancount is easily extensible with plugins that provide additional semantics or importers for data import, e.g., from online banking services.

Furthermore, there is Fava, the semi-official Web UI for Beancount.

Why rewrite it in Rust?

So, if Beancount is so great, why rewrite it in Rust?

For one, Beancount can struggle a bit with larger input files. For this reason, among others, there is an also an internal rewrite going on, that aims to replace the core with a C++ implementation.

Another issue I've faced with Beancount is some instability and frustration with plugins. Sometimes, new versions of Beancount or Fava remove old APIs, and the plugins may take a while to catch up. For me, being not that experienced with the Python tooling, it's hard to downgrade to an earlier, consistent state of packages when I run into issues. And let's not even mention the fun to be had when the system python version changes and I get to spend an afternoon rebuilding my Fava environment from scratch.

Lastly, and this is the major reason for me to do this, I wanted a project to practice writing Rust. With something that I use frequently, it's quick to get feedback. And with Beancount and Fava being available, I can incrementally replace parts of my workflow and fall back to the original tools when my tool has not yet progressed far enough. Also, for me, something I've written myself in Rust is easier to customize than writing a plugin in Python.

My main design goal for the rewrite was to have an easily maintainable and extensible codebase. Secondary goals were a good performance and robustness.

What's the progress so far?

With that out of the way, what's the current progress so far?

You can check the project repo for yourself, but as of writing this, I have implemented a basic framework for writing and running importers and a number of importers in it. I've also implemented a number of building blocks, such as libraries of different data types used in Beancount.

However, there is still a number of missing features.

  • There is no support for downloading statements yet.
  • I've not yet implemented or integrated a parser for existing Beancount files, so for now, importers may overwrite existing transactions.
  • I've also not implemented deduplication on the framework level yet, so imports may result in duplicate files.

What's next?

Next, I want to start publishing parts of this on crates.io. I'm currently polishing beancount-account, which I will likely publish in the next few days.

I also want to tackle the missing features, starting with a parser for Beancount files. Once that is integrated into the framework, I can implement deduplication of transactions, both in a single import run as well as with existing transactions.

Once we have a parser, another next step could be implementing booking in the file. From there, report generation and user interfaces become feasible.

At some point, I want to implement a plugin system for the framework, both for transaction processing as well as importers. However, this needs some more design work, since I want to make sure to avoid the issues listed above.

I'm interested to hear from you: Did you know Beancount already? Are you interested in a rewrite? What features would you need from somthing like this?

If you have any comments or feedback on this post, or just would like to ask a question, shoot me an email at korrat@proton.me.


1039 Words

2023-10-29