https://blog.kaleidos.net/penpot-chose-clojure-as-its-language-and-here-is-why/
title
Kaleiders What we do Our Work Culture Kaleidos Startups Contact Blog
Home >>
Penpot chose Clojure as its language and here's why
Andrey Antukh profile picture
* Andrey Antukh
* 16/03/2022
* Open source
* Design
* English
* Clojure
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.
[Juan_and_A]
Juan de la Cruz and myself at the pre-Covid Kaleidos' office
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.
[Penpot_PIW]
First version of UXBOX (Penpot's original concept) in 2016
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).
[Penpot_202]
Current Penpot interface
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}) {
2 let button;
3 if (isAuth) {
4 button =