Skip to content

Instantly share code, notes, and snippets.

@danielcompton
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielcompton/bd07ce46d14d5849be57 to your computer and use it in GitHub Desktop.
Save danielcompton/bd07ce46d14d5849be57 to your computer and use it in GitHub Desktop.

Clojure Reader Conditionals by Example

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

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

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

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*))))))

Splicing

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])]))))

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment