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 socket 與資料庫通訊。相反,我們使用讀取條件式,因此只有在 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

在讀取條件被引入之前,透過名為 cljx 的 Leiningen 外掛程式解決了在平台之間共用程式碼的相同目標。cljx 處理具有 .cljx 副檔名的檔案,並將多個特定於平台的檔案輸出到產生的來源目錄。然後,Clojure 讀取器 會將它們讀取為正常的 Clojure 或 ClojureScript 檔案。這運作良好,但需要執行另一個工具組件。cljx 在 2015 年 6 月 13 日被棄用,改用讀取條件。

Sente 以前使用 cljx 在 Clojure 和 ClojureScript 之間共用程式碼。我已改寫 main 命名空間以使用讀取條件。請注意,我們已使用拼接讀取條件將向量拼接至父 :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