Tags: clojure Type: post
One of the most interesting features of the upcoming Clojure 1.7 release is Reader Conditionals. These are designed to allow different variants of Clojure to share common logic, while also writing platform specific code in the same file.
Previously this was solved by cljx which processed a Clojure file with a .cljx extension and output multiple platform specific files. These were then read as normal by the Clojure Reader. This worked well, but required another piece of tooling to run, and it wasn't able to be used in Clojure Core projects.
Reader Conditionals are an answer from the Clojure Core team to that problem. They are similar in spirit and appearance to cljx, however they are integrated into the Clojure Compiler, and don't require any extra tooling beyond Clojure 1.7 or greater. To use Reader Conditionals, your file must have a .cljc extension.
There are two types of reader conditionals, standard and splicing. The standard conditional reader behaves similarly to a traditional cond
. The syntax for usage is #?
and looks like:
#?(:clj (Clojure expression)
:cljs (JavaScript expression)
:clr (Clojure CLR expression)
The syntax for a splicing conditional read is #?@
. It is used to splice lists into the containing form. So the Clojure reader would read this:
(defn build-list []
(list #?@(:clj [5 6 7 8]
:cljs [1 2 3 4])))
as this:
(defn build-list []
(list 5 6 7 8))
One important thing to note is that in Clojure 1.7 a splicing conditional reader cannot be used to splice in multiple top level forms (tracked in CLJ-1706). In concrete terms, this means you can't do this:
;; Don't do this!, will throw an error
#?@(:clj
[(defn clj-fn1 [] :abc)
(defn clj-fn2 [] :cde)])
;; CompilerException java.lang.RuntimeException: Reader conditional splicing not allowed at the top level.
Instead you'd need to do this:
#?(:clj (defn clj-fn1 [] :abc))
#?(:clj (defn clj-fn2 [] :cde))
Let's go through some examples of places you might want to use these new reader conditionals:
Host interop is one of the biggest pain points that cljx and reader conditionals solve. You may have a Clojure file that is almost pure Clojure, but needs to call out to the host environment for one function. This is a classic example:
(defn str->int [s]
#?(:clj (java.lang.Integer/parseInt s)
:cljs (js/parseInt s)))
Namespaces are the other big pain point for sharing code between Clojure and ClojureScript. ClojureScript has different syntax for requiring macros than Clojure. To use these macros in a .cljc
file, you'll need Reader Conditionals in the namespace declaration.
I saw Vesa Karvonen had a neat trick for this, taking advantage of the fact you can have multiple :require
's in one ns form.
(ns poc.cml.sem
(#?(:clj :require :cljs :require-macros)
[poc.cml.macros :refer [go sync!]])
(:require
[poc.cml :refer [choose wrap]]
[poc.cml.util :refer [chan on put!]]))
Here is a larger example showing more conventional usage from a test in route-ccrs
(ns route-ccrs.schema.ids.part-no-test
(:require #?(:clj [clojure.test :refer :all]
:cljs [cljs.test :refer-macros [is]])
#?(:cljs [cljs.test.check :refer [quick-check]])
#?(:clj [clojure.test.check.clojure-test :refer [defspec]]
:cljs [cljs.test.check.cljs-test :refer-macros [defspec]])
#?(:clj [clojure.test.check.properties :as prop]
:cljs [cljs.test.check.properties :as prop
:include-macros true])
[schema.core :as schema :refer [check]]
[route-ccrs.schema.ids :refer [PartNo]]
[route-ccrs.generators.part-no
:refer [gen-part-no gen-invalid-part-no]]))
Sente uses CLJX for sharing code between Clojure and ClojureScript. I've rewritten the main namespace in reader conditionals. Notice that we've used the splicing reader conditional to splice the vector into the parent :require
.
(ns taoensso.sente
(:require
#?@(:clj [[clojure.string :as str]
[clojure.core.async :as async]
[taoensso.encore :as enc]
[taoensso.timbre :as timbre]
[taoensso.sente.interfaces :as interfaces]]
:cljs [[clojure.string :as str]
[cljs.core.async :as async]
[taoensso.encore :as enc]
[taoensso.sente.interfaces :as interfaces]]))
#?(:cljs (:require-macros
[cljs.core.async.macros :as asyncm :refer (go go-loop)]
[taoensso.encore :as enc :refer (have? have have-in)])))
In this example, we want to be able to use the rethinkdb.query
namespace in Clojure and ClojureScript. However we can't use the rethinkdb.net
namespace that it requires in ClojureScript as it uses Java sockets to communicate with the database. Instead we use a reader conditional so it's only required when read by Clojure programs.
(ns rethinkdb.query
(:require [clojure.walk :refer [postwalk postwalk-replace]]
[rethinkdb.query-builder :as qb :refer [term parse-term]]
#?(:clj [rethinkdb.net :as net])))
;; snip...
#?(:clj (defn run [query conn]
(let [token (get-token conn)]
(net/send-start-query conn token (replace-vars query)))))
Exception handling is another area that benefits from reader conditionals. ClojureScript now supports (catch :default) to catch everything and this may come to Clojure too. However even then there will still be times when you want to operate on the host specific exception that is generated. Here's an example from lemon-disc.
(defn message-container-test [f]
(fn [mc]
(passed?
(let [failed* (failed mc)]
(try
(let [x (:data mc)]
(if (f x) mc failed*))
(catch #?(:clj Exception :cljs js/Object) _ failed*))))))
I don't see a lot of uses yet for the splicing reader conditional outside of namespace declarations, but to get really meta, lets look at the tests for reader conditionals in the ClojureCLR reader. What might not be obvious at first glance, is that the lists of [:a :b :c]
are actually being spliced into the parent wrapping list.
(deftest reader-conditionals
;; snip
(testing "splicing"
(is (= [] [#?@(:clj [])]))
(is (= [:a] [#?@(:clj [:a])]))
(is (= [:a :b] [#?@(:clj [:a :b])]))
(is (= [:a :b :c] [#?@(:clj [:a :b :c])]))
(is (= [:a :b :c] [#?@(:clj [:a :b :c])]))))
At the time of writing, I'm not aware of any way to use .cljc
files in versions of Clojure less than 1.7, nor is there any porting mechanism to preprocess .cljc
files like CLJX does. For that reason library maintainers may need to wait for a while until they can safely drop support for older versions of Clojure and adopt reader conditionals.
If you have any other interesting uses for reader conditionals, let me know and I'll update this post to add them.
Lastly I'd like to offer my congratulations to everyone who worked on Reader Conditionals, and its predecessor Feature Expressions. There was a lot of thought and discussion put into this, and I think that the process has produced something useful, extensible, and long-lasting.