#?(:clj (Clojure expression)
:cljs (ClojureScript expression)
:cljr (Clojure CLR expression)
:default (fallthrough expression))
阅读条件是在 Clojure 1.7 中添加的。它们旨在允许不同版本的 Clojure 共享大部分平台无关的代码,但包含一些平台相关的代码。如果您正在跨多个平台编写大部分独立的代码,则应将 .clj
和 .cljs
文件分开。
阅读条件集成到 Clojure 阅读器中,不需要任何额外的工具。要使用阅读条件,您只需让您的文件具有 .cljc
扩展名。阅读条件是表达式,可以像普通的 Clojure 表达式一样操作。有关更多技术细节,请参阅关于 阅读器 的参考页面。
阅读条件有两种类型:标准和拼接。标准阅读条件的行为类似于传统的 cond
。使用语法是 #?
,如下所示
#?(:clj (Clojure expression)
:cljs (ClojureScript expression)
:cljr (Clojure CLR expression)
:default (fallthrough expression))
平台标签 :clj
等是硬编码到每个平台中的固定标签集。:default
标签是一个众所周知的标签,用于在没有平台标签匹配时捕获并提供表达式。如果没有任何标签匹配,并且没有提供 :default
,则阅读条件将不读取任何内容(不是 nil,而是像从流中根本没有读取任何内容一样)。
拼接阅读条件的语法是 #?@
。它用于将列表拼接到包含的形式中。因此,Clojure 阅读器将读取此内容
(defn build-list []
(list #?@(:clj [5 6 7 8]
:cljs [1 2 3 4])))
作为此
(defn build-list []
(list 5 6 7 8))
需要注意的一点是,在 Clojure 中,拼接条件阅读器不能用于拼接多个顶级形式。具体来说,这意味着您不能执行此操作
;; 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.
相反,您需要分别包装每个函数
#?(:clj (defn clj-fn1 [] :abc))
#?(:clj (defn clj-fn2 [] :cde))
或使用 do
来包装所有顶级函数
#?(:clj
(do (defn clj-fn1 [] :abc)
(defn clj-fn2 [] :cde)))
让我们来看一些可能需要使用这些新阅读条件的示例。
主机互操作是阅读条件解决的最大痛点之一。您可能有一个几乎是纯 Clojure 的 Clojure 文件,但需要调用主机环境的一个函数。 这是一个经典的例子
(defn str->int [s]
#?(:clj (java.lang.Integer/parseInt s)
:cljs (js/parseInt s)))
命名空间是 Clojure 和 ClojureScript 之间共享代码的另一个主要痛点。ClojureScript 在 要求宏 方面的语法与 Clojure 不同。要在 .cljc
文件中使用在 Clojure 和 ClojureScript 中都可用的宏,您需要在命名空间声明中使用阅读条件。
这是一个来自 测试的示例,该示例位于 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.properties :as prop]
:cljs [cljs.test.check.properties :as prop
:include-macros true])
[schema.core :as schema :refer [check]]))
另一个例子是,我们希望能够在 Clojure 和 ClojureScript 中使用 rethinkdb.query
命名空间。但是,我们无法在 ClojureScript 中加载所需的 rethinkdb.net
,因为它使用 Java 套接字与数据库通信。相反,我们使用阅读条件,以便仅在 Clojure 程序读取时才要求命名空间。
(ns rethinkdb.query
(:require [clojure.walk :refer [postwalk postwalk-replace]]
#?(: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)))))
异常处理是另一个受益于阅读条件的领域。ClojureScript 支持 (catch :default)
来捕获所有内容,但您通常仍然需要处理主机特定的异常。这是来自 示例,该示例位于 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*))))))
拼接阅读条件不像标准阅读条件那样被广泛使用。为了说明它的用法,让我们看看 ClojureCLR 阅读器中关于阅读条件的 测试。乍一看可能并不明显,但拼接阅读条件中的向量被一个周围的向量包装。
(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])]))))
关于放置 .cljc
文件的位置还没有明确的社区共识。两种选择是创建一个包含 .clj
、.cljs
和 .cljc
文件的 src
目录,或者创建单独的 src/clj
、src/cljc
和 src/cljs
目录。
在引入阅读条件之前,使用 Leiningen 插件 cljx 可以实现相同的跨平台共享代码的目标。cljx 处理具有 .cljx
扩展名的文件,并将多个平台特定文件输出到一个生成的源代码目录中。然后,这些文件被 Clojure 阅读器作为普通的 Clojure 或 ClojureScript 文件读取。这工作得很好,但需要运行另一段工具。cljx 于 2015 年 6 月 13 日被弃用,取而代之的是阅读条件。
Sente 以前使用 cljx 在 Clojure 和 ClojureScript 之间共享代码。我重写了 主 命名空间以使用阅读条件。请注意,我们使用了拼接阅读条件将向量拼接到了父 :require
中。还要注意,一些要求在 :clj
和 :cljs
之间重复。
(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)])))
(ns taoensso.sente
#+clj
(:require
[clojure.string :as str]
[clojure.core.async :as async)]
[taoensso.encore :as enc]
[taoensso.timbre :as timbre]
[taoensso.sente.interfaces :as interfaces])
#+cljs
(:require
[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)]))
原始作者:Daniel Compton