Isomorphic Clojure[Script]

Part 2 (Portable Code)

The most fundamental abstraction necessary for building isomorphic applications is code portability. We need to be able to write once and run in the server and browser. In a perfect world, we would write a language with identical syntax, semantics, and platform features. But that’s unrealistic, so let’s aim for similar syntax and semantics as well as an easy way to specify platform-specific units of code.

Our goal is for isomorphic Clojure[Script] applications to be more performant and easier to develop than their JavaScript counterparts. Outperforming JavaScript applications is easy; even V8 can’t touch the JVM when it comes to runtime performance and React with persistent data structures blows everything else away. But making isomorphic Clojure[Script] applications easier to develop than JavaScript ones is no small task.

While JavaScript on the server and in the browser are not identical in features, they are identical in syntax and semantics when they share features, which is not something that can be said of Clojure and ClojureScript. One option would be to throw Clojure out and only use ClojureScript by running it in Node on the backend. This is a completely valid option, but sacrifices JVM performance and the plethora of excellent Clojure and Java libraries that are available.

Let’s define the terms: An isomorphic Clojure[Script] application has a Clojure (JVM) backend and a ClojureScript (JavaScript) frontend where both sides of the application are capable of user-visible functionality like routing and rendering. This is the definition that I had in mind when writing Omelette. This architecture comes with more challenges than that of isomorphic JavaScript or isomorphic ClojureScript but the potential benefits make it worth pursuing. This post will focus on that of code portability. We want to write code that is portable; it will run in Clojure and ClojureScript and produce identical (or, in some cases, similar) results.

Sharing Portable Code

We need a way to share portable code so that we only have to write it once.

Just Copy It

The most obvious approach is to maintain multiple files per namespace; one file for each platform. Just copy the code from “namespace.clj” to “namespace.cljs” and modify the platform-specific parts.

But that’s just…gross! Let’s never speak of this again.

Use lein-cljsbuild crossovers

This tool copies “.clj” files to “.cljs” files. It’s better than the prior approach which shall not be named. However, its macro sharing functionality is clumsy and it totally ignores platform differences. For example, crossovers won’t help unify (Float/parseFloat "3.14") in Clojure with (js/parseFloat "3.14") in ClojureScript.

Crossovers is now deprecated in favor of cljx.

cljx

This is what is used in Omelette. Given “namespace.cljx”, cljx outputs “namespace.clj” and “namespace.cljs” files. Forms prepended with #+clj and #+cljs are only output to “.clj” and “.cljs” files respectively. As with lein-cljsbuild crossovers, cljx is a source-to-source compiler. Unlike lein-cljsbuild crossovers, cljx is more granular – compiling by form instead of file. This is the best solution that is currently available.

Reader Conditionals

Reader conditionals are a proposed feature in Clojure 1.7. You can read more about the design decisions and discussion on dev.clojure.org.

The current proposal satisfies the use case for cljx but differs in syntax and functionality. Unlike cljx, reader conditionals do not output new files; they extend the reader, allowing it to selectively read forms based on the current platform’s features.

Reader conditionals introduce two new literal forms: #? (read-cond) and #[email protected] (read-cond-splicing). The former reads a single expression and the latter reads a list and splices it into the parent form. Here’s an example:

(ns my.namespace
  (:require [foo.bar #[email protected](:cljs [:include-macros true])]
            [#?(:clj clojure.core.async :cljs cljs.core.async) :as csp])
  #?(:cljs (:require-macros [cljs.core.async.macros :as csp])))

Challenges

The fact that we can target the JVM and JavaScript from a single codebase is remarkable but is not without its challenges.

(set/intersection Clojure ClojureScript)

ClojureScript is not a subset of Clojure. It contains functionality that Clojure lacks. Portable code must target the intersection of Clojure and ClojureScript. That means no extend from Clojure or specify (an incredibly useful but underutilized function) from ClojureScript.

Macros

ClojureScript lacks native macros but is able to make use of Clojure macros. This is undoubtedly useful but is also lacking in several ways.

  • Since ClojureScript lacks macros but can use Clojure macros, any platform-specific macro functionality will need to be implemented using conditionals and the contents of &env. (Within a macro, (:ns &env) is only truthy when expanding ClojureScript code.)
  • Macros are Clojure’s only facility for doing work before runtime and are incredibly useful for optimizing performance without sacrificing syntax. The ability to do this in ClojureScript is significantly diminished because macros in ClojureScript (which are Clojure macros) cannot call ClojureScript functions.
  • Since ClojureScript macros are really Clojure macros, symbols are resolved in Clojure. For example, map would resolve to clojure.core/map instead of cljs.core/map. The only way around this faulty resolution is to quote and then unquote the symbol, like ~'map.

Core Implementation

Clojure is primarily implemented with Java classes and interfaces in clojure.lang.*. ClojureScript is primarily implemented with types and protocols in cljs.core/*. This makes it cumbersome to extend core functionality.

For example, let’s say we have a protocol that we want to extend to maps:

(defprotocol KeywordKeys
  (keyword-keys [this] "Returns a seq of keys that are also keywords."))

In Clojure, we can extend it to the IPersistentMap interface:

(extend-protocol KeywordKeys
  clojure.lang.IPersistentMap
  (keyword-keys [this] (filter keyword? (keys this))))

In ClojureScript, we can only extend to concrete types because ClojureScript lacks interfaces and protocols cannot be extended to other protocols:

(extend-protocol KeywordKeys
  PersistentArrayMap
  (keyword-keys [this] (filter keyword? (keys this)))
  PersistentHashMap
  (keyword-keys [this] (filter keyword? (keys this))))

Namespaces

ClojureScript namespace declarations significantly differ from Clojure namespace declarations.

  • ClojureScript has all sorts of extras for dealing with macros like :require-macros and :include-macros. The Clojure ns macro does not know how to deal with these extensions.
  • ClojureScript intentionally lacks :use and :refer :all. The ClojureScript ns macro does not know how to deal with these features.
  • Due to implementation differences, Clojure types are brought into a namespace with :import but ClojureScript types are brought into a namespace with :require and :refer.
  • ClojureScript namespaces are inconsistently named (cljs.core vs. clojure.core).
  • ClojureScript lacks some of Clojure’s namespaces like clojure.edn and Clojure lacks some of ClojureScript’s namespaces like cljs.reader.
  • Unlike other parts of ClojureScript which can be made to be more similar to Clojure via macros, namespaces are completely closed to user extension because ClojureScript lacks require.

All of these issues combine to make namespaces declarations the most clumsy and redundant part of portable code.

Host Interop

Clojure’s standard library lacks common functionality like parseInt. This is fine when running only on the JVM but causes pain when writing portable code due to Java and JavaScript inconsistencies. Since java.lang.* is imported into clojure.core but global JavaScript properties are accessible as js/*, even interop where Java and JavaScript are consistent require Clojure and ClojureScript forms that are inconsistent.

Additionally, even though Java and JavaScript ostensibly share some core types, they are often referenced differently in ClojureScript than in Clojure. For example, you extend-protocol to String in Clojure but string in ClojureScript. Similar pain occurs around exception handling and type hinting, since those parts of Clojure and ClojureScript are, at most, a paper-thin wrapper over their host platforms.

Concurrency

Clojure’s concurrency primitives were designed with the JVM in mind and therefore require blocking. Since JavaScript is non-blocking, ClojureScript lacks Clojure’s future, promise, agent, and ref concurrency primitives because they can block on deref. Without using any external libraries, writing portable concurrent code is tough.

Positives

core.async

Move “Concurrency” over to the positives because core.async rocks! It is tremendously helpful because it lets us write roughly the same asynchronous code in Clojure or ClojureScript despite ClojureScript neither having threads or being able to block. This is huge!

The community has been justifiable excited about core.async. While it is great in Clojure or ClojureScript, it really shines when writing portable code.

It works!

I can still barely believe that I am able to write virtually the same syntax and utilize Clojure’s incredible features while enjoying JavaScript’s incredible reach. Run everywhere JavaScript runs but write Clojure instead of JavaScript? Yes, please!