Reagent Making a subnetting calculator with ClojureScript

An Experinment with Reagent and ClojureScript

Published: Sat, 20 Apr 2024 09:58:20

After having used Emacs for over five years now, I have been becoming increasingly interested in trying out Lisp dialects other than Emacs Lisp (or "Elisp"). My first impression of Lisp was one of confusion. I found it utterly incomprehensible, and most of my time spent configuring Emacs was copying and pasting snippets of code from other people's setups or documentation. In truth most of my Emacs configuration still is lifted from one place or another, but about six months ago I heard someone say "everything in Elisp is an expression", and for some reason hearing that short phrase made Elisp click for me. When I realized that what came between a set of parentheses was a function, or more accurately a symbolic expression, and that that expression returned a value to the set of parentheses one level up, everything just clicked.

Right away I started putting this new found knowledge to work by going back to parts of my Emacs configuration that I had not touched in a long time and fixing things up. I took my first crack at a minor mode by creating emmet-react-mode and worked a simple Elisp function into my blog workflow. I would still like to continue my work with Emacs Lisp, for example by creating a more fully featured blogging minor mode, yet I found myself intrigued by Lisp and finally ready to start exploring other Lisps.

Enter Clojure

I spent several months paralyzed by indecision before I actually started to write any code. I wanted to take the time to deliberate over which of the various Lisp dialects I would like to try first. I even briefly considered trying out Fennel to do some work on my Neovim configuration, but I didn't use Neovim enough to find the motivation to try this (though I would be surprised if I didn't do this eventually). Finally I spent some time looking more seriously into Clojure and realized that it had several features that interested me.

The main thing that stood out to me about Clojure was the runtime, or runetimes. Not only does it run on the ubiquitous Java Virtual Machine, but it can also compile down to the even more ubiquitous JavaScript. In addition to the runtime systems, Clojure is also interoperable with both Java and JavaScript libraries: in short, all of the libraries. Clojure is also a functional style language, which intrigued me. I had been interested for some time to try out a functional language in addition to a Lisp, so I was effectively killing two birds with one stone by trying Clojure. Don't worry: I am a vegetarian, and no actual birds were harmed in the making of this application.

Once I had decided on Clojure, I started reading Clojure for the Brave and True, which is both hilarious and a great book even it can be a bit dense at times, Web Development with Clojure, which covers building a guestbook application with the Luminus framework, and consuming a healthy diet of Rich Hickey talks on YouTube.

Normally I am an advocate for starting simple, but in the case of Clojure I find the process of building web applications a bit complicated to set up. The code required to set up a web application from scratch using Ring and getting ClojureScript to play nicely and cross compile with Clojure is a bit daunting to a Clojure neophyte, let alone someone relatively new to programming in Lisp in any form. After spending some time with Luminus (and if I am being honest, finding all the boilerplate very confusing), I finally settled on making a single page Reagent application with the Kit framework. I would recommend trying Kit for someone who is interested to try out Clojure web development for the first time. I was able to get working with a simple hello world Reagent app within a few minutes and the project structure and boilerplate code is well laid out and relatively easy to understand. There are comments throughout that basically say "Put code here".

IP Subnetting Calculator

Deciding on the topic for my first Clojure/Script project was a little like putting on a blindfold and playing darts. I didn't really care what I made, partly because I knew that my first attempts with the language would probably be relatively naive, yet I wanted to make something. Several months ago I had played with the idea of making a subnetting calculator when I was working through some of the Cisco CCNA course content in Cisco Packet tracer. The way a subnet mask is applied to an IP address to create subnets intrigued me, and I thought that creating an application that displayed both the decimal notation information alongside the actual 32 bit representation of the IP addresses in binary form would be an excellent way to learn more about it and do a bit of an IPv4 deep dive. I did learn a bit more about subnets by doing the project, specifically how CIDR /31 and /32 subnets work alongside with their purpose in a network, so I think the project was well worth the time even for that.

As expected, my implementation of the application was indeed quite naive, which led to a lot of issues throughout the project ( GitHub repo ), but it was quite instructive and will help me to build better Reagent applications in the future.

Atoms

A core feature of a Reagent application is a specially modified version of a Clojure atom. Atoms are how Reagent controls state in an application. Whenever an atom is de-refenced within a Reagent render function (a render function is similar to how you return jsx in a function style React component), Reagent will update that part of the dom whenever the state of that atom changes elsewhere in the application. As these atoms need to be de-referenced in each render function you want them to update in, you need to be very intentional about where and how you define atoms in a similar way to how you need to carefully structure how props are passed between React components.

When I initially started making the application, and it was not clear to me how atoms worked in Clojure let alone Reagent, I defined a total of 8 separate atoms in my main component: one for each byte in an IPv4 address and one for each byte in a subnet mask. This created two problems; first I had to pass all 8 atoms to each child component, and second I needed to pass all 8 atoms to each function that needed to work on both the IP address and subnet mask. Needless to say, this became cumbersome incredibly quickly. In the end I opted for what I thought was the least bad option, which was to define the 8 atoms in the global scope of the Reagent name space, and then pass the atoms to the functions that did the various calculations instead of trying to pass them to each child component. As such, I had to be quite diligent with how I manged the atoms.

If I made the application again, or if I take the time to rewrite it, I will definitely be more intentional about how I define the atoms, and I will probably define them as either maps or vectors within the main component, pass them to the child components and then deference them within the child components.

Sequences

Another mistake I made was to design the functions that did the decimal/binary conversions in such a way that they returned strings in the format "1 1 0 0 0 0 0 0" or "1 1 1 1 0 0 0 0" etc. instead of returning a sequence of numbers, such as a list or a vector.

(defn calc-bits ([dec-str]
                 "Takes a decimal number between 0 and 255 and returns
a string representing the binary bits."
                 (cond
                   (> dec-str 255) (str "Max is 255")
                   (< dec-str 128) (calc-bits dec-str "0 " 6)
                   (>= dec-str 128) (calc-bits (- dec-str 128) "1 " 6)))
  ([dec-str bit-str bit-pos]
   (if (= bit-pos 0)
     (str bit-str dec-str)
     (if (< dec-str (exp 2 bit-pos))
       (recur dec-str (str bit-str "0 ") (- bit-pos 1))
       (recur (- dec-str (exp 2 bit-pos)) (str bit-str "1 ") (- bit-pos 1))))))

The function takes a decimal string, taken in this case from user input, and then recursively generates the string by testing whether the remaining total of the decimal number was greater than the bit position's power of 2 value. This works quite well, and served as a useful abstraction throughout the application, but doing further calculations required that I use clojure.string/split throughout in order to do calculations on the bits, such as applying the subnet mask, determining the host and last bit etc. Had I used vectors, then I could have used numbers instead of strings for easier calculations and make a simple function to generate a string from the vector.

It's possible that my approach was slightly more efficient because only the elements that contain that specific atom need to update, but the drawbacks of having to pass around so many atoms certainly outweigh that minor benefit.

Takeaways

Despite the mistakes and challenges that I encountered, my overall first impression of both Clojure and Reagent has been very positive. The combination of a functional approach combined with Lisp allows you to make quite elegant and concise functions from fairly simple building blocks. I think this might be what makes Clojure and perhaps Lisp quite daunting to read at times when you are first learning it as a lot can be going on in a fairly small snippet of code. As you start to learn various building blocks of the language and how to effectively define and operate on sequences, you start to find yourself quickly writing more concise and effective code.

Reagent itself is likely to quickly become my favorite way to write React code, at least for personal projects. It's minimalist approach and simplicity can be deceptive; not only does it seem capable for creating complex UIs, but of doing so in a way that is quite ergonomic. Furthermore, the REPL driven development creates a very effective feedback loop for developing. In fact, I wrote the entire application without even bothering to use the Clojure language server. That's not to say you should not use the LSP to write Clojure, but the REPL driven development is in itself powerful enough that I didn't miss having it enough to even bother turning it on or put up with it yelling at me. I probably would use it with a more complex application if only for the benefit of better code navigation.

Despite it's strengths, I did notice some drawbacks to working with Reagent and ClojureScript. What stood out the most was the relative complexity of a ClojureScript project. your code is compiling both to run on the JVM, but also to JavaScript to run in the browser, There are separate REPLs for Clojure and ClojureScript on account of their differing runtimes and, as I mentioned before, the overall project structure and boilerplate required to make all of this work in concert is significant when compared to setting up an application with, for example, NextJS or SvelteKit. Notwithstanding this complexity, Kit does an excellent job of structuring the project skeleton and setting all of this up so that you can start writing Reagent code with just a few minutes of set up time. Furthermore the benefits outweigh, in my opinion at this time at least, this drawback. As for my opinion of how this scales with more complex applications, only time will tell, but I am sure that I will continue my Clojure journey and find out in time.

System Crafters Web Ring

Messing around with computers and coding since I was 8. Now getting paid to do what I love.