James Crake-Merani

<- Return home

Creating a simple calculator with Reagent, and ClojureScript

By James Crake-Merani. Published on 08/08/2023


Introduction

I created a basic online calculator using the ClojureScript language, and the Reagent wrapper of React. The source code is available on GitHub

Background

I've been learning Clojure, a Lisp dialect which compiles into the Java Virtual Machine (JVM). Not only does it benefit from having access to the whole Java ecosystem, it is also in my opinion a good dialect of Lisp in itself. There also exists ClojureScript, a dialect of Clojure which transpiles into JavaScript rather than compiling into JVM bytecode. This is great because you get to combine all the benefits of coding in a Lisp with all the benefits of developing JavaScript code whose interpreters are ubiquitous these days thanks to web browsers.

React is a popular JavaScript framework, and I really like it because of, well, its reactive nature. State can become the source of much complexity in programs, and thus React is a really handy tool to handle this complexity by making components automatically re-render when the state changes meaning that there is no risk of components presenting data to the user that is not in sync with the program's internal state. While it is possible to use React straight from ClojureScript, its much easier to use a wrapper which is better suited to the intricacies of a language like ClojureScript. Several wrappers exist, but the one I've chosen to go with is Reagent.

In the place of React hooks, Reagent uses its own version of Clojure atoms with the key difference is that atoms remember which components rely on their state, and when the state of the atom changes, the components dependent on it re-render automatically. This is certainly a more unique way of handling state compared to the vanilla React way of doing it using hooks but I think it works well. The only criticism I have is that the Reagent docs seem to like putting atoms in the global scope rather than within functions, and I don't really like this as it feels akin to global variables in other languages. The workaround is to pass atoms around to the components, and functions that need them. This seemed to work for my usage, although initially I was passing around so many different atoms that I eventually created a 'state' map which had all of my state atoms rolled into one map to make passing them around much easier.

(let [state {:left-value (r/atom nil)
               :right-value (r/atom nil)
               :operation (r/atom nil)
               :result (r/atom nil)}]
    ...)

For the HTML page the project is hosted on, I've used Water CSS. This overrides the ugly browser default styles with some nicer ones but is designed to be lightweight, and doesn't require you to give your elements any classes like you would with a UI library like Bootstrap. Furthermore, I've also heard the author was only 13 years old when she developed this library which is seriously impressive!

Challenges

I can remember learning to setup the Shadow-CLJS build system being a bit of a tricky task although I can't remember all the fine details. One detail I do remember (which took me a while to fix) was that Shadow-CLJS expects to find your code file in a directory structure which matches the namespace of whatever you've told Shadow-CLJS. However, although its customary to use hyphens in Clojure namespaces, you're not supposed to use these in directory names. Rather, you need to substitute these with an underscore. I didn't initially know this, and it took a while of getting confusing error messages before I had worked out what the problem is. I initially used a Leiningen project when I was playing around with Reagent but I was told that Leiningen isn't really needed these days because, I understand, Clojure has most of the functionality that you used to need Leiningen for. The project structure I went with does work in the end so I'll probably stick with it for any further projects I do with Reagent.

Learning Reagent itself didn't really feel like much of a challenge; it seemed fairly straightforward. Since I was already familiar with React, and the concepts behind it, most of my knowledge transferred over, and although Reagent uses Hiccup instead of JSX, it is basically just HTML with some different syntax. I was already used to writing HTML-like code in Lisp (including the markup for the website you're looking at now).

Most of the challenge in this project was designing the state. I decided to go for the approach of having the calculator be in several states: the user would start typing in the value for the left hande side of their operation, they would enter an operation (e.g. addition), and then they would enter the right hand side of their calculation before clicking on the equals button, and being presented with their result. You can see there are several stages that the application can be in.

The approach I went for was that (as can be seen by the previous code listing), each stage of the cycle would have its own state atom, and the application would judge which state it is by which atoms currently hold a value. This solution worked however it presented some challenges. Firstly, I allowed users to enter their values in two ways: they could either press the buttons on the page, or enter numbers through their keyboard. In the latter approach, I added an event handler for every key press rather than whenever the input value changes because if I had gone with the latter, it was going to give me all the content that I was displaying within that box including all the formatting, and then I would have to interpret that value to pull out the information I needed in order to update the state. This I thought would be complicated so the former approach had me just getting the key that the user typed in, and updating the state based on that. However, this solution was much more inflexible. The user couldn't, for example, select all the text in the box, and delete it, and I also noticed that entry to this box doesn't work on mobile (although its unnecessary in this scenario since the user is much more likely to just use the buttons). These issues are inherent in the solution that I choose, and thus in order to fix them I would've have to had gone for a different solution. Perhaps interpreting the value entered into the box rather than generating it based on state?

One glaring omission in the Clojure standard library to me is the fact that there is no simple function to test whether an item is in a sequence. To make matters more confusing, there is a function named contains? but it has a completely different purpose. The workaround often recommended is to use the some function, as demonstrated below

(some #{5} '(1 2 3 4 5 6 7 8 9 10))
-> true 

And it sort of works (and works properly for my use case in this application) but it still feels like this is a weird function to exclude from the standard library especially considering that many other lisps implement a member function. I have to say, although I do like Clojure, there are some other examples of missing features from the standard library that make working within it a bit awkward. Another example is that there doesn't appear to be a function to parse in a number so I had to use the edn/read-string function as a workaround. I could've used the JavaScript interop but I don't really like doing this because I want to be able to write Clojure code independent of whether it is running in the JVM, or by a JavaScript interpreter.

Going Forward

I'm really excited to try out doing some more projects with Reagent as I found it a very useful tool. I'm also interested in perhaps doing some experimentation with Tailwind CSS as well, and perhaps learning to build user interfaces with that.

I have joked in the past that I have become an accidental web developer because I've also had to do a lot of backend development with the Flask micro-framework, and now this. But the thing is: I think browsers have rapidly overtaken technologies like the Java Virtual Machine in providing the 'write once, run anywhere' functionality especially given how standardised web browsers are these days compared to prior years. The only way I can guarantee my program is going to work at most of the machines I throw at it is to write that program for the web browser, and I see that can be lamentable because we're missing out on the performance benefits of native applications; browsers are notorious for hogging up lots of RAM on the user's machine, and browser apps do tend to be slower than native apps. But I also, like many, have limited time, and I don't want to constantly be rewriting my applications for different operating systems, and for mobile. The web seems to just be the easiest, and fastest way we can get our cool ideas into the hands of users.


<- Return home