Why Clojure? Part 2: A developer on-boarding perspective

Prior to co-founding RAMS, I worked for 15 years as a backend Java developer. My experience came from working in Fintech, a space known for hard engineering problems where challenges like scalability, low latency and memory optimization made a lot of difference.

During that period, I became proficient in Java development and took on multiple managerial roles working closely with young developers during their on-boarding process. I remember clearly the discussions I used to have with them about “best practices”. Most of the times “best practices” were simply “constraints”: things that that they should avoid doing even if they are allowed by the language. This reality hit me harder when I started learning Clojure. In fact, I realized that most of the things I used to tell the young developers NOT to do in Java were not even there in Clojure to begin with. As if the language was designed to “protect” the beginners and make it hard for them to make mistakes. As if the approaches that come with years of experience and “wisdom” in Java are just there at language level by design. In this article, I will explain a bit more what I used to coach the young Java developers on and how it compares to Clojure’s built-in ways.

Two very important notes regarding this article:

1- Tip 1: Limit visibility of class fields

All new developers to Java are first introduced to a mainstream IDE. In my case it usually was IntelliJ or Eclipse. Immediately, they would start using the amazing shortcuts made available to them and among other the famous “Generate Getters/Setters” one. Without them knowing it, this would result in making all the objects they create mutable. Here, I would usually draw their attention to the importance of adding the “final” keyword (when possible) for their fields as this would restrict the IDE to creating the “Getters” only and protect them from the risks of mutability.

In Clojure, the above scenario is not even possible. Whether you are using the builtin data structures (most commonly maps and lists) or the very few class-like ones (eg. defrecord and deftype) it is not possible to change the value of a variable after it has been defined. So there you go, we do not even have a problem to begin with!

2- Tip 2: Avoid using function parameters as “output”

Another common discussion I used to have with young Java developers was function parameters. I used to stress on the importance of having a clear function contract to understand what are the “inputs” and the “output”. Although this looks simple on the surface, many times I used to notice that young developers would actually confuse these two concepts. They would for example pass a list as a parameter to a function and have the function “fill it”! The problem with this is that developers end up losing the concept of input and output. Look at the below example:

In the above example, the developer is splitting the numbers in a given list depending on whether they are even or odd. The code works perfectly well but the only problem is that it is breaking the basic concept explained above. The even and odd variables are input parameters but behave more like an output (!!) while the function is “void”. Clearly a much better approach would be to have the function return both lists as a result.

If you take the above scenario in Clojure, than the above beginner mistake would not be possible! Why? Because it is not possible to have a function modify the input parameters it receives! The result of this constraint would naturally push the developer towards a similar solution:

Now you may be saying: we could’ve written it the same way in Java. You are right! The only difference is that in Clojure you had no other choice but to write it the right way ;)

3- Tip 3: Avoid mutating function parameters or retrieved class fields

Any sizable Java program contains a function that receives a Map as input or an object that returns a List in a Getter.

As a beginner, you may be tempted to modify a Map that you receive at your code level. You could maybe add something to it to have it available somewhere else in your program. The problem with this is that you are modifying the variable for your caller without their consent! This perceived “flexibility” is in fact a problem for your caller.

The above issues push programmers to go “defensive” to protect their mutable data structures. As an example, I have seen the following two approaches by experienced programmers:

1- Wrap a mutable Map with Collections.unmodifiableMap() before passing it on to someone else. In this case, if the receiving code attempts to change the Map an UnsupportedOperationException would be thrown. While this clearly solves the problem it is really a frustrating experience for the receiving party for two reasons:

2- Copy/Clone their Map before passing it on. In this case, if the receiving code attempts to change the Map, they do not modify the original one. This again solves the problem. However, the problem with this is that most of the time developers modify the Map in order to view the impact of their change somewhere else in their program. Failing to do so will result in confusion and potentially minutes (and sometimes hours) of troubleshooting time.

What about Clojure in this case? Well, any collection operation in Clojure results in a “new” collection. The language thus eliminates the problem at the source. The caller can pass on their Map to someone else “worry-free”!

Conclusion

Java’s flexibility is best used by experienced programmers. I find that the language does not do a lot to prevent beginners from making basic mistakes. On the other side, Clojure puts more constraints on the developers by promoting immutability. While this feels constraining for developers (who usually learn the imperative style of programming that promotes mutability), it has a lot of benefits and is actually less error-prone.

Note: the recent versions of Java introduce new class-like structures. Java 14 introduced records for example. However, I still feel that the bigger part of the community is still exposed to the class-oriented approaches. It will take a lot of time to convert the attitude of the people who are used to the language .