This is probably the question we’ve been asked the most at Penpot. We have a vague explanation in our FAQ page, so with this article I’ll try to explain a bit the motivations and spirit behind each decision. The rationale is multi-faceted.
It all started in a PIWEEK. Of course!
A small team, at one of our Personal Innovation Weeks (PIWEEK) in 2015, began the project from the initial idea of making an open-source prototyping tool and released a working prototype after a week of hard work and lots of fun (you can check it here: no credentials needed, just hit the login button). This was the first static prototype, i.e. without a backend). Personally, I was not part of the initial team.
There were many reasons to choose ClojureScript back then (around December 2015) to build a prototype for a hackathon in just one week but I think the most important reason was probably because it was fun. It offered a functional paradigm (in the team there was a lot of interest in functional languages) and back then it provided a fully interactive development environment.
I’m happy to say that the current situation hasn’t changed that much. I’m not referring to post-compilation browser auto-refresh, I mean refreshing the code at runtime without doing a page refresh and without losing state! Think about this, technically you could be developing a game and change the behavior of the game while you are still playing it, just by touching a few lines in your editor. With ClojureScript (and Clojure) you don’t need anything fancy—the language constructs are already designed with hot reload in mind from the ground up.
I know, today (in 2022) you can also have something similar with React on plain JS and probably with other frameworks that I’m not familiar with (there are too many and each one claims to be the best one). But it is also probable that this capability support is more limited and fragile because of intrinsic limitations of the language—sealed modules, no proper REPL, etc.—where hot reload was a later addition (not direct language support, only with external tools and constructions).
About REPL
You may be wondering: yes, but isn’t this something that all other languages already have? In other dynamic languages like Javascript, Python, Groovy… in my experience, these REPL features are added as an afterthought so they have issues with hot reloading or language patterns that being ok in real code are not suitable in the REPL (example: const in JS is evaluating the same code twice in a REPL).
These REPLs are usually used merely to test code snippets specifically made for the REPL. In contrast, REPL usage in Clojure rarely involves typing or copying into the REPL directly, and it is much more common to evaluate small code snippets from the actual source files. These are frequently left in the code base as comment blocks so you can use the snippets in the REPL again when you change the code in the future.
What I mean here is that in the Clojure REPL, you can literally develop the entire application without any limitations, because the Clojure REPL doesn’t behave differently from the compiler itself. You’re able to do all kinds of runtime introspection and hot replacement of specific functions in any namespace in an already running application. In fact, it is not uncommon to find backend applications in a production environment to expose REPL on a local socket to be able to inspect the runtime and, if necessary, patch specific functions without even having to restart the service. It’s quite nice to be able to streamline deployment IF needed.
From the prototype to the usable application
Then, after that PIWEEK in 2015, I and my friend and colleague, Juan de la Cruz (designer at Penpot and the original author of the project idea), picked up the project in our spare time and with all the lessons learned from the first prototype we rewrote the entire project. After hard work throughout 2016, at the beginning of 2017 we released internally what could be called the second MVP or functional prototype—this time with a backend. And the thing is, I was back to using Clojure and ClojureScript for this stage too!
The initial reasons were still valid and relevant, but the motivation for such an important time investment implied other reasons. It’s a very long list but I think the most important of all were: stability, backwards compatibility and syntactic abstraction (macros).
Stability and backwards compatibility
The stability and backwards compatibility are one of the most important goals of the language. There is usually not much of a rush to include all the trendy stuff into the language without having tested its real usefulness. And it’s not uncommon to see people running production on top of an alpha version of the Clojure compiler. Because **it is very rare to have instability issues even on alpha releases. **
“Contrarily, in the JS world, if you see a library that doesn’t have commits in a few months, you already get the feeling that the library is abandoned or unmaintained.”
In Clojure or Clojurescript, if a library doesn’t have commits for some time, it is most likely fine as-is. It needs no further development and works perfectly on the latest version of the Clojure compiler. Contrarily, in the JS world, if you see a library that doesn’t have commits in a few months, you already get the feeling that the library is abandoned or unmaintained. And in fact, it is most likely broken with the latest version of NodeJS or the framework of the day.
There are numerous times when I’ve downloaded a JS project that has not been touched in 6 months and I’ve found more than half of the stuff already deprecated and not maintained. On other occasions, it doesn’t even compile because some dependencies have not respected the semantic versioning.
This is why each dependency on Penpot is carefully chosen, with continuity, stability and backwards compatibility in mind, and many of them have been developed in-house. Only delegating to third-party libraries when they have proven to have these properties or the effort/time ratio of doing it in-house wasn’t worth it.
I think the summary can be that we try to have the minimum necessary external dependencies. React is probably a good example of a big external dependency. Over time, it has shown that they have a real concern with backwards compatibility, where each major release incorporates the changes gradually and with a clear path for migration, being able to almost always allow for old and new code to coexist. But let’s not get off topic, because this could be another article on its own.
Syntactic abstractions
Another reason is clearly sintactic abstractions (macros). It is one of those characteristics that, as a general rule, can be a double-edged sword. You must use it carefully and not abuse it. But with the complexity of Penpot’s project, having the ability to extract certain common or verbose constructs has helped us simplify the code. These statements cannot really be generalized, and the possible value they provide must be seen on a case-by-case basis. Here I’ll try to explain a couple of important instances for us:
- When we began building Penpot, React only had components as a class. But those components were modeled as functions and decorators in a rumext library. When React released versions with hooks that greatly enhanced the functional components, we only had to change the implementation of the macro and 90% of Penpot’s components could be kept unmodified. Subsequently, we have gradually moved from decorators to hooks completely without the need for a tiresome migration. This reinforces the same idea of the previous paragraphs: stability and backwards compatibility.
- The second most important case, I think, is the ease of using native language constructs (vectors and maps) to define the structure of the virtual DOM instead of using a JSX-like custom DSL. Using those native language constructs would make a macro end up generating the corresponding calls to React.createElement at compile time, still leaving room for additional optimizations. Obviously, the fact that the language is expression-oriented makes it all more idiomatic.
Let’s see a starting point example in js, mainly based on examples from React documentation:
1 | function MyComponent({isAuth, options}) { |
Now, the equivalent in Clojurescript:
1 | (defn my-component [{:keys [auth? options]}] |
All these data structures used to represent the virtual DOM will be converted into the appropriate React.createElement
calls at compile time.
The fact that Clojure is so data oriented made using the same native data structures of the language to represent the virtual DOM a totally natural and logical process. Don’t forget, Clojure is a dialect of LISP, where the syntax and AST of the language use the same data structures and can be processed with the same mechanisms.
Funnily enough, working with React through Clojurescript feels much more natural than with JS. All the extra tools that are added to React to use it comfortably — JSX, immutable data structures or tools to work with data transformations and state handling — come as part of the language in ClojureScript.
Guest Language
And finally, one of the fundamentals of Clojure and ClojureScript is that they were built as GUEST languages, that is, they work on top of an existing platform/runtime. In this case, Clojure is built on top of the JVM and ClojureScript on top of JS, which means that the interoperability between the language and the runtime is very efficient. This allowed us to take advantage of the entire ecosystem of both Clojure plus everything that is done in Java (the same for ClojureScript and JS). Also, there are pieces of code that are easier to write and reason about if they are written in imperative languages like Java or JavaScript. Clojure can coexist with them in the same code base without any problems!
Not to mention the ease of sharing code between frontend and backend even though each one can be running in a completely different runtime (JS and JVM). At Penpot, almost all the most important logic for managing a file’s data is written in code and is executed both in the frontend and in the backend.
Perhaps, just perhaps, you could say we have chosen what some people might call a boring technology but without actually being boring at all!
Trade-offs
Obviously, every decision has trade-offs, and the choice to use Clojure/ClojureScript is not an exception. From a business perspective, the choice of Clojure could be seen as risky because it is not a mainstream language, with a relatively small community compared to Java or JavaScript, and finding developers is inherently more complicated.
But, in my opinion, the learning curve is much lower than it might seem at first glance. Once you get rid of the “it’s different” fear (or as I jokingly call it: fear of the parentheses), you start to have fluency with the language very quickly. There are tons of learning resources out there, both books and training courses.
Actually, the real obstacle that I have noticed is the paradigm shift, rather than the language itself. At Penpot, jokes aside, the necessary and inherent complexity of the project makes the programming language the least of our problems when facing development: building a design platform is no small feat.
If you’d like to know more about what decisions and technologies, in my opinion, have contributed to Penpot’s amazing development cycle and stability, I hope to answer your questions in future articles.
Sign up for free and give Penpot a try.