Clojure

阅读条件指南

简介

阅读条件是在 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/cljsrc/cljcsrc/cljs 目录。

cljx

在引入阅读条件之前,使用 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