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.
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.
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.
This is what is used in Omelette. Given “namespace.cljx”, cljx outputs “namespace.clj” and “namespace.cljs” files. Forms prepended with
#+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 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:
#[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])))
(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.
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,
mapwould resolve to
cljs.core/map. The only way around this faulty resolution is to quote and then unquote the symbol, like
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
(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))))
ClojureScript namespace declarations significantly differ from Clojure namespace declarations.
- ClojureScript has all sorts of extras for dealing with macros like
:include-macros. The Clojure
nsmacro does not know how to deal with these extensions.
- ClojureScript intentionally lacks
:refer :all. The ClojureScript
nsmacro does not know how to deal with these features.
- Due to implementation differences, Clojure types are brought into a namespace with
:importbut ClojureScript types are brought into a namespace with
- ClojureScript namespaces are inconsistently named (
- ClojureScript lacks some of Clojure’s namespaces like
clojure.ednand Clojure lacks some of ClojureScript’s namespaces like
- 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
All of these issues combine to make namespaces declarations the most clumsy and redundant part of portable code.
Clojure’s standard library lacks common functionality like
java.lang.* is imported into
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.
ref concurrency primitives because they can block on
deref. Without using any external libraries, writing portable concurrent code is tough.
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.