I’ve recently been tasked with writing an interactive web app called Shotbot to help mobile app developers quickly create attractive App Store/Google Play Store store screenshots. Instead of using JavaScript like most other web apps, I’ve decided to write this one in Clojure.
Why?
Clojure is a compiled, functional and dynamically typed language that targets multiple existing runtimes. At the time of writing, Clojure can run on the Java JVM, .NET CLR and JS engines.
ClojureScript refers to the Clojure → JS compiler but within the community it’s also used for Clojure code that runs on JS engines (i.e. doesn’t contain Java API calls).
Clojure is also a dialect of LISP, this is a design decision based on some interesting properties of LISP to make the language more extensible (explained later). The only thing you need to know about LISP for now is that for every function call, put the open parenthesis in front of the function name and stack all closing ones on the same line:
// non-LISP function(arg) // nested calls a( b( c() ) )
;; LISP (function arg) ;; nested calls (a (b (c)))
I have previously used Clojure in other projects to write back-end servers and loved the experience. Now that I finished developing Shotbot, I’m confident to recommend Clojure to front-end developers as well. Here’s why:
The Simple Ecosystem
If you’ve developed a front-end application that’s less than trivial in the past few years, you’ve probably experienced the JS community’s chaotic landscape (best described in How it feels to learn JavaScript in 2016) caused by cancerous growth, everyone wants to build a new tool that works for them and the problem just gets worse from there…
Clojure’s ecosystem on the other hand has grown at a much steady pace over the last 10 years with strong foundations, it had the time to refine its tools such that it works for most developers. Here’s a quick comparison of tooling choice decisions that you may have to make when starting a new front-end project today:
JavaScript | Clojure | |
---|---|---|
Project Scaffolding | Grunt / Slush / Yeoman | Leiningen |
Build Tool | Webpack / Grunt / Gulp / Browserify | Leiningen |
Package Manager | npm / yarn / bower | Leiningen |
Major Framework | Angular / Ember / React / Vue / Backbone / Knockout | om.next / reagent/re-frame |
No matter what you are building, you may use Leiningen for most cases in Clojure.
Seamless JS Interop
Host interoperability is one of the main design goals of the Clojure language. Unlike some of the newer languages that aims to improve on existing ones (such as Dart from the same developers of the v8 JS engine), Clojure presents host runtime entities as first-class citizens.
By using Clojure, you could leverage both existing JS and Clojure libraries or even write part of your app in JS for performance reasons. In fact, ShotBot contains JS code doing pixel manipulation for 3D transformations.
Here’s some basic Interop code to give you a taste:
;; Clojure code (ns my.namespace) ;; print using JS console (def my-js-obj #js{:foo "bar"}) ;; var my_js_obj = {foo: 'bar'}; (.log js/console my-js-object) ;; console.log(my_js_object); ;; => {foo: 'bar'} ;; JS-callable function (defn ^:export add ;; function add(a, b) { (+ a b)) ;; return a + b; ;; } ;; NOTE: there are no operators in Clojure, "+" is just a normal function
// JS code my.namespace.add(7, 11); // => 18
You don’t need to worry too much about locking yourself within a separated language and ecosystem with Clojure, interop just works without the need for foreign function interfaces (FFI).
It’s a Well-Designed Language
If you have worked on any sizeable JS code base, you have probably dealt with some of JS’s language quirks such as function context (this), hoisting, prototype inheritance, number array sorting, and the list goes on….
Unlike JS which is designed in the 90’s for simple web page scripting, Clojure aims to tackle complex distributed systems in the simplest way possible while still being extensible. It achieves this by providing several abstractions that are baked into the language itself:
Abstraction of Behavior
Entities in the Clojure language are not explicitly bound to their behavior. A mechanism known as protocol is used to decouple concrete entities from what you could do with them, this works in a similar fashion to Java/C# interfaces or Scala traits. This enables you to write functions that explicitly declare the behavioral expectations of its arguments instead some complex opaque type.
The built-in get
function works on anything that implements the ILookup
protocol:
(get [4 5 6] 0) ;; => 4 (get {:a 1 :b 2} :a) ;; => 1 (get "abc" 2) ;; => "c"
Anything that implements the IFn
protocol can be called like a function:
(def my-map {:a 1 :b 2}) (get my-map :a) ;; => 1 (my-map :a) ;; => 1 (:a my-map) ;; => 1
By the way, the “I” in front of the protocol names stands for “Interface”. Clojure was first implemented in Java and they were actually interfaces.
Abstraction of Syntax
Normally, if you want new language features, you need to fork the compiler / interpreter and make some patches. With Clojure, you don’t.
Remember back in the beginning I mentioned something about LISP having special properties? This is what I was talking about: in any LISP language, code is just data (or more specifically, code is a subset of data that conforms to a language spec). To illustrate this:
(foo bar baz qux)
- code:
- call the function bound to the name “foo” with the values bound to the names “bar”, “baz” and “qux”.
- data:
- list of 4 symbols, “foo”, “bar”, “baz” and “qux”.
A bit of a late introduction but parentheses are the LISP syntax for lists and LISP is short for List Processor. Every program can been seen as a bunch of nested lists which means you’ve been explicitly writing out the abstract syntax tree (AST) of your program all along.
Okay, code is just data, so what?
The magical bit comes when you can actually run some functions at compile-time, they can take your code-data as argument and return transformed code-data. These are known as macros and they are actually compiler extensions that allow you to implement new language features without having touch the actual compiler. The best part is that you can import and use macros just like any normal function:
;; Clojure doesn't support string interpolation... ;; not the case if you import a macro for it! (def foo 1) (<< "foo is ~{foo}") ;; => "foo is 1" ;; Don't like LISP's prefix maths notation? ;; Write a macro for infix maths! (infix (3 + 4 * 12) / 5) ;; => 10.2
Clojure even has go-lang style CSP channels implemented as macros!
Abstraction of Implementation
Clojure is a high level language, we often want to code as if we’re writing a formal specification for what the program does instead of dealing with the implementation details.
Just like how you would use control structures instead of Goto, in Clojure we often use high level functions instead of control structures. That’s why everything looks like function calls in Clojure code.
To illustrate this here’s a function directly taken from my ShotBot project that checks if a confirmation prompt is needed when the user tries to leave the page by seeing if all screenshots are saved:
(fn confirm-leave? (when-not (->> (vals (:shot-transient-states db)) (every? :saved?)) "This page contains unsaved data, sure you want to leave?"))
The (https://clojuredocs.org/clojure.core/vals)
function extracts all values from a map and the (https://clojuredocs.org/clojure.core/every_q)
function checks if a predicate is true for all. If you were to write this function without them in JS it would probably look something like this:
function comfirmLeave(db) { let allSaved = true; for(let shotID in db.shotTransientStates) { if(!db.shotTransientStates.saved) { allSaved = false; break; } } if(allSaved) { return null; } else { return 'This page contains unsaved data, sure you want to leave?'; } }
The usage of high level functions is one of the key concepts of functional programming and they act like reusable building blocks for your program much like Tetris pieces.
By using Clojure in your next web project, you can not only mitigate most of JS’ language quirks but also finish the same job with less code!
Getting Started with Clojure
I hope you’ve enjoyed this piece, and if you’d like to get started with Clojure — Here’s some online interactive exercises where you could get your hands wet with Clojure: