Clojure

リーダーコンディショナルガイド

はじめに

リーダーコンディショナルはClojure 1.7で追加されました。プラットフォームに依存しない共通コードを、プラットフォームに依存する一部のコードと共に、Clojureの異なる方言間で共有できるように設計されています。複数のプラットフォームにわたって、ほとんど独立したコードを記述する場合は、代わりに`.clj`ファイルと`.cljs`ファイルを分離する必要があります。

リーダーコンディショナルはClojureリーダーに統合されており、追加のツールは必要ありません。リーダーコンディショナルを使用するには、ファイルの拡張子が`.cljc`である必要があります。リーダーコンディショナルは式であり、通常のClojure式のように操作できます。技術的な詳細については、リーダーに関するリファレンスページを参照してください。

リーダーコンディショナルには、標準とスプライシングの2種類があります。標準のリーダーコンディショナルは、従来の`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))

重要な点の1つは、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)))

これらの新しいリーダーコンディショナルを使用したい場所の例をいくつか見てみましょう。

ホストとの相互運用

ホストとの相互運用は、リーダーコンディショナルによって解決される最大の悩みの種の1つです。ほとんど純粋なClojureであるClojureファイルがあるかもしれませんが、1つの関数のためにホスト環境を呼び出す必要があります。これが典型的な例です。

(defn str->int [s]
  #?(:clj  (java.lang.Integer/parseInt s)
     :cljs (js/parseInt s)))

名前空間

名前空間は、ClojureとClojureScript間でコードを共有する際のもう1つの大きな悩みの種です。ClojureScriptは、Clojureとは異なるマクロのrequire構文を持っています。`.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`名前空間を使用できるようにしたいとします。ただし、必要な`rethinkdb.net`は、Javaソケットを使用してデータベースと通信するため、ClojureScriptにロードできません。代わりに、リーダーコンディショナルを使用して、Clojureプログラムによって読み取られる場合にのみ名前空間がrequireされるようにします。

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

例外処理

例外処理は、リーダーコンディショナルの恩恵を受けるもう1つの領域です。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`ファイルをどこに配置するかについて、明確なコミュニティのコンセンサスはまだありません。2つのオプションは、`.clj`、`.cljs`、および`.cljc`ファイルを含む単一の`src`ディレクトリを持つこと、または個別の`src/clj`、`src/cljc`、および`src/cljs`ディレクトリを持つことです。

cljx

リーダーコンディショナルが導入される前は、プラットフォーム間でコードを共有するという同じ目標は、cljxと呼ばれるLeiningenプラグインによって解決されていました。cljxは`.cljx`拡張子を持つファイルを処理し、複数のプラットフォーム固有のファイルを生成されたソースディレクトリに出力します。これらは、Clojure リーダーによって通常のClojureまたはClojureScriptファイルとして読み取られました。これはうまく機能しましたが、実行するには別のツールが必要でした。cljxは、リーダーコンディショナルを支持して2015年6月13日に廃止されました。

Senteは以前、ClojureとClojureScript間でコードを共有するためにcljxを使用していました。 メイン名前空間をリーダーコンディショナルを使用するように書き直しました。ベクターを親の`:require`にスプライスするために、スプライシングリーダーコンディショナルを使用したことに注意してください。また、一部の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