Clojure

リーダー

Clojureはホモアイコニックな言語です。これは、ClojureプログラムがClojureのデータ構造で表現されているという事実を表す、やや専門的な用語です。これは、Clojure(およびCommon Lisp)と他のほとんどのプログラミング言語との非常に重要な違いです。Clojureは、文字ストリーム/ファイルの構文ではなく、**データ構造の評価**という観点から定義されています。Clojureプログラムが他のClojureプログラムを操作、変換、生成することは非常に一般的であり、容易です。

とはいえ、ほとんどのClojureプログラムはテキストファイルとして始まります。そして、*リーダー*の役割は、テキストを解析して、コンパイラが認識するデータ構造を生成することです。これはコンパイラの単なる一段階ではありません。リーダーとClojureのデータ表現は、XMLやJSONなどを使用する多くの同じコンテキストで独自のユーティリティを持っています。

リーダーは文字の観点から定義された構文を持ち、Clojure言語はシンボル、リスト、ベクター、マップなどで定義された構文を持つと言えるかもしれません。リーダーは関数readで表され、ストリームから次のフォーム(文字ではなく)を読み取り、そのフォームで表されるオブジェクトを返します。

どこから始めなければならないかと言うと、このリファレンスは評価が始まる場所、つまりリーダーフォームから始まります。これは必然的に、記述の詳細とコンパイラによる解釈が続くデータ構造について話すことを伴います。

リーダーフォーム

シンボル

  • シンボルは数値以外の文字で始まり、英数字、*、+、!、-、_、'、?、<、>、=を含むことができます(最終的には他の文字も許可される可能性があります)。

  • '/'には特別な意味があります。名前空間と名前を区切るために、シンボルの真ん中で一度使用できます。例:my-namespace/foo。'/'単体では除算関数を表します。

  • '.'には特別な意味があります。完全修飾クラス名(例:java.util.BitSet)や名前空間名で、シンボルの真ん中で1回以上使用できます。'.'で始まりまたは終わるシンボルは、Clojureによって予約されています。'/'または'.'を含むシンボルは、「修飾済み」と呼ばれます。

  • ':'で始まりまたは終わるシンボルは、Clojureによって予約されています。シンボルには、重複しない':'を1つ以上含めることができます。

リテラル

  • 文字列 - "二重引用符"で囲まれています。複数行にまたがる場合があります。標準的なJavaエスケープ文字がサポートされています。

  • 数値 - 一般的にJavaと同じように表現されます

    • 整数は無限に長く、範囲内であればLongとして、それ以外の場合はclojure.lang.BigIntとして読み込まれます。Nサフィックスが付いた整数は常にBigIntとして読み込まれます。8進表記は0プレフィックスで、16進表記は0xプレフィックスで使用できます。可能な場合は、2から36までの基数で任意の基数で指定できます(Long.parseLong()を参照)。例えば、2r1010100528r520x2a36r1642はすべて同じLongです。

    • 浮動小数点数はDoubleとして読み込まれます。Mサフィックスが付いている場合はBigDecimalとして読み込まれます。

    • 有理数はサポートされています。例:22/7

  • 文字 - バックスラッシュで始まります:\c\newline\space\tab\formfeed\backspace\returnは、対応する文字を生成します。Unicode文字はJavaのように\uNNNNで表現されます。8進数は\oNNNで表現されます。

  • nil は「何もない/値がない」ことを意味し、Javaのnullを表し、論理的に偽であると判定されます。

  • ブール値 - truefalse

  • シンボリック値 - ##Inf##-Inf##NaN

  • キーワード - キーワードはシンボルに似ていますが、

    • コロンで始まり、コロンで始まる必要があります。例::fred

    • 名前の部分に'.'を含めることはできません。また、クラスを命名することもできません。

    • シンボルと同様に、名前空間を含めることができます(:person/name)。名前空間には'.'を含めることができます。

    • 2つのコロンで始まるキーワードは、現在の名前空間で自動的に解決され、修飾されたキーワードになります。

      • キーワードが修飾されていない場合、名前空間は現在の名前空間になります。user名前空間では、::rect:user/rectとして読み込まれます。

      • キーワードが修飾されている場合、名前空間は現在の名前空間のアリアスを使用して解決されます。xexampleにエイリアスされている名前空間では、::x/foo:example/fooに解決されます。

リスト

リストは、括弧で囲まれた0個以上のフォームです:(a b c)

ベクター

ベクターは、角括弧で囲まれた0個以上のフォームです:[1 2 3]

マップ

  • マップは、波括弧で囲まれた0個以上のキー/値のペアです:{:a 1 :b 2}

  • コンマは空白と見なされ、ペアを整理するために使用できます:{:a 1, :b 2}

  • キーと値は任意のフォームにすることができます。

マップの名前空間構文

Clojure 1.9で追加

マップリテラルは、#:nsプレフィックスを使用して、マップ内のキーのデフォルトの名前空間コンテキストをオプションで指定できます。ここで、*ns*は名前空間の名前であり、プレフィックスはマップの開始波括弧{の前にあります。さらに、自動解決キーワードと同じセマンティクスで名前空間を自動的に解決するために#::を使用できます。

名前空間構文を持つマップリテラルは、名前空間を持たないマップとは以下の点で読み込みが異なります。

  • キー

    • 名前空間を持たないキーワードまたはシンボルであるキーは、デフォルトの名前空間を使用して読み込まれます。

    • 名前空間を持つキーワードまたはシンボルであるキーは、特別な名前空間_を除いて影響を受けません。これは読み込み中に削除されます。これにより、名前空間構文を持つマップリテラルで、名前空間を持たないキーワードまたはシンボルをキーとして指定できます。

    • シンボルまたはキーワードでないキーは影響を受けません。

    • 値は影響を受けません。

    • 入れ子になったマップリテラルのキーは影響を受けません。

たとえば、次の名前空間構文を持つマップリテラル

#:person{:first "Han"
         :last "Solo"
         :ship #:ship{:name "Millennium Falcon"
                      :model "YT-1300f light freighter"}}

は次のように読み込まれます。

{:person/first "Han"
 :person/last "Solo"
 :person/ship {:ship/name "Millennium Falcon"
               :ship/model "YT-1300f light freighter"}}

セット

セットは、#で始まる波括弧で囲まれた0個以上のフォームです:#{:a :b :c}

deftype、defrecord、およびコンストラクタ呼び出し(バージョン1.3以降)

  • Javaクラス、deftype、およびdefrecordコンストラクタへの呼び出しは、#で始まる完全修飾クラス名とベクターで呼び出すことができます:#my.klass_or_type_or_record[:a :b :c]

  • ベクター内の要素は、**評価されずに**関連するコンストラクタに渡されます。defrecordインスタンスは、マップを取る同様のフォームでも作成できます:#my.record{:a 1, :b 2}

  • マップ内のキー付き値は、**評価されずに**defrecord内の関連フィールドに割り当てられます。リテラルマップに対応するエントリがないdefrecordフィールドには、nilが値として割り当てられます。マップリテラル内の余分なキー付き値は、結果のdefrecordインスタンスに追加されます。

マクロ文字

リーダーの動作は、組み込みの構成要素と、読み込みテーブルと呼ばれる拡張システムの組み合わせによって制御されます。読み込みテーブルのエントリは、マクロ文字と呼ばれる特定の文字から、リーダーマクロと呼ばれる特定の読み込み動作へのマッピングを提供します。特に明記されていない限り、マクロ文字はユーザーシンボルでは使用できません。

クォート(')

'form(quote form)

文字(\)

上記のように、文字リテラルを生成します。例として、\a \b \cがあります。

一般的な文字には、次の特殊な文字リテラルを使用できます:\newline\space\tab\formfeed\backspace\return

Unicodeサポートは、基盤となるJavaバージョンに対応するサポートを使用して、Javaの規則に従います。Unicodeリテラルは\uNNNN形式です。たとえば、\u03A9はΩのリテラルです。

コメント(;)

単一行コメント。セミコロンから行末までのすべてをリーダーが無視します。

デリファレンス(@)

@form ⇒ (deref form)

メタデータ(^)

メタデータは、いくつかの種類のオブジェクトに関連付けられたマップです:シンボル、リスト、ベクター、セット、マップ、IMetaを返すタグ付きリテラル、およびレコード、型、コンストラクタ呼び出し。メタデータリーダーマクロは、最初にメタデータを読み取り、次に読み取るフォームに添付します(オブジェクトにメタを添付するにはwith-metaを参照)。
^{:a 1 :b 2} [1 2 3]は、{:a 1 :b 2}のメタデータマップを持つベクター[1 2 3]を生成します。

ショートハンドバージョンでは、メタデータを単純なシンボルまたは文字列にすることができます。その場合、キーが:tagで値が(解決された)シンボルまたは文字列の単一エントリマップとして扱われます。例:
^String x^{:tag java.lang.String} xと同じです。

このようなタグを使用して、コンパイラに型情報を伝えることができます。

別のショートハンドバージョンでは、メタデータをキーワードにすることができます。その場合、キーがキーワードで値がtrueの単一エントリマップとして扱われます。例:
^:dynamic x^{:dynamic true} xと同じです。

メタデータは連結でき、その場合は右から左にマージされます。

ディスパッチ(#)

ディスパッチマクロにより、リーダーは後続の文字によってインデックス付けされた別のテーブルからのリーダーマクロを使用します。

  • #{} - 上記のセットを参照

  • 正規表現パターン(#"pattern")

    正規表現パターンは読み込み時にコンパイルされます。生成されるオブジェクトはjava.util.regex.Pattern型です。正規表現文字列は、文字列と同じエスケープ文字規則に従いません。具体的には、パターン内のバックスラッシュはそのまま扱われ(追加のバックスラッシュでエスケープする必要はありません)、例えば、(re-pattern "\\s*\\d+")#"\s*\d+"とより簡潔に記述できます。

  • Varクォート (#')

    #'x(var x)

  • 匿名関数リテラル (#())

    #(…​)(fn [args] (…​))
    ここで、argsは、%、%n、%&の形式をとる引数リテラルの存在によって決定されます。%は%1の同義語であり、%nはn番目の引数(1ベース)、%&はrest引数を指定します。これはfnの代替ではありません。慣習的な使い方は、非常に短い使い捨てのマッピング/フィルタ関数などです。#()形式はネストできません。

  • 次のフォームを無視する (#_)

    #_ の後のフォームは、リーダーによって完全にスキップされます。(これは、nilを返すcommentマクロよりも完全な削除です)。

シンタックスクォート(`、注:「バッククォート」文字)、アンクォート(~)、アンクォートスプライシング(~@)

シンボル、リスト、ベクター、セット、マップ以外のすべてのフォームについて、`xは'xと同じです。

シンボルの場合、シンタックスクォートは現在のコンテキストでシンボルを解決し、完全修飾シンボル(つまり、名前空間/名前またはfully.qualified.Classname)を生成します。シンボルが名前空間修飾されておらず、 '#'で終わる場合、 '_'と一意のIDを追加した同じ名前の生成されたシンボルに解決されます。例:x#はx_123に解決されます。シンタックスクォートされた式内のそのシンボルへのすべての参照は、同じ生成されたシンボルに解決されます。

リスト/ベクター/セット/マップの場合、シンタックスクォートは対応するデータ構造のテンプレートを確立します。テンプレート内では、修飾されていないフォームは再帰的にシンタックスクォートされたかのように動作しますが、アンクォートまたはアンクォートスプライシングで修飾することで、そのような再帰的なクォートからフォームを除外できます。その場合、それらは式として扱われ、それぞれ、テンプレート内でそれらの値または値のシーケンスに置き換えられます。

例:

user=> (def x 5)
user=> (def lst '(a b c))
user=> `(fred x ~x lst ~@lst 7 8 :nine)
(user/fred user/x 5 user/lst a b c 7 8 :nine)

読み込みテーブルは現在、ユーザープログラムからアクセスできません。

拡張可能なデータ表記(EDN)

Clojureのリーダーは、拡張可能なデータ表記(EDN)のスーパーセットをサポートしています。EDN仕様は現在開発中であり、言語に依存しない方法でClojureデータ構文のサブセットを定義することで、このドキュメントを補完します。

タグ付きリテラル

タグ付きリテラルは、EDNのタグ付き要素のClojure実装です。

Clojureが起動すると、クラスパスのルートでdata_readers.cljまたはdata_readers.cljcという名前のファイルが検索されます。そのような各ファイルには、次のようなClojureマップのシンボルが含まれている必要があります。

{foo/bar my.project.foo/bar
 foo/baz my.project/baz}

各ペアのキーは、Clojureリーダーによって認識されるタグです。ペアの値は、タグの後のフォームを解析するためにリーダーによって呼び出されるVarの完全修飾名です。たとえば、上記のdata_readers.cljファイルがあると、Clojureリーダーは次のフォームを解析します。

#foo/bar [1 2 3]

#'my.project.foo/bar Varをベクター[1 2 3]で呼び出すことによって。データリーダー関数は、リーダーによって通常のClojureデータ構造として読み取られた後のフォームで呼び出されます。独自のデータリーダー関数では、エラー情報を提供するメッセージを含むRuntimeExceptionのインスタンスをスローすることでエラーを報告する必要があります。

名前空間修飾子がないリーダータグは、Clojure用に予約されています。default-data-readersでデフォルトのリーダータグが定義されていますが、data_readers.clj/data_readers.cljcでオーバーライドしたり、*data-readers*を再バインドしたりできます。タグに対応するデータリーダーが見つからない場合、*default-data-reader-fn*にバインドされている関数が、タグと値を使用して値を生成するために呼び出されます。*default-data-reader-fn*がnilの場合(デフォルト)、RuntimeExceptionがスローされます。

data_readers.cljcが提供されている場合、他のcljcソースファイルと同じセマンティクスで、リーダー条件付きで読み取られます。

組み込みタグ付きリテラル

Clojure 1.4では、instantUUIDのタグ付きリテラルが導入されました。インスタンスの形式は#inst "yyyy-mm-ddThh:mm:ss.fff+hh:mm"です。注:この形式の要素の一部は省略可能です。詳細はコードを参照してください。デフォルトのリーダーは、提供された文字列をデフォルトでjava.util.Dateに解析します。例:

(def instant #inst "2018-03-28T10:48:00.000")
(= java.util.Date (class instant))
;=> true

*data-readers*はバインドできる動的変数であるため、デフォルトのリーダーを別のリーダーに置き換えることができます。たとえば、clojure.instant/read-instant-calendarはリテラルをjava.util.Calendarに解析し、clojure.instant/read-instant-timestampjava.util.Timestampに解析します。

(binding [*data-readers* {'inst read-instant-calendar}]
  (= java.util.Calendar (class (read-string (pr-str instant)))))
;=> true

(binding [*data-readers* {'inst read-instant-timestamp}]
  (= java.util.Timestamp (class (read-string (pr-str instant)))))
;=> true

#uuidタグ付きリテラルは、java.util.UUIDに解析されます。

(= java.util.UUID (class (read-string "#uuid \"3b8a31ed-fd89-4f1b-a00f-42e3d60cf5ce\"")))
;=> true

デフォルトのデータリーダー関数

タグ付きリテラルを読み取るときにデータリーダーが見つからない場合、*default-data-reader-fn*が呼び出されます。独自のデフォルトのデータリーダー関数を設定でき、提供されているtagged-literal関数は、未処理のリテラルを格納できるオブジェクトを構築するために使用できます。tagged-literalによって返されるオブジェクトは、:tag:formのキーワード検索をサポートしています。

(set! *default-data-reader-fn* tagged-literal)

;; read #object as a generic TaggedLiteral object
(def x #object[clojure.lang.Namespace 0x23bff419 "user"])

[(:tag x) (:form x)]
;=> [object [clojure.lang.Namespace 599782425 "user"]]

リーダー条件付き

Clojure 1.7では、複数のClojureプラットフォームでロードできるポータブルファイルの新しい拡張子(.cljc)が導入されました。プラットフォーム固有のコードを管理するための主なメカニズムは、そのコードを最小限の名前空間のセットに隔離し、その後、それらの名前空間のプラットフォーム固有のバージョン(.clj/.classまたは.cljs)を提供することです。

コードの異なる部分を隔離することが不可能な場合、またはコードがほとんどポータブルで、プラットフォーム固有の部分がごくわずかしかない場合、1.7ではリーダー条件付きも導入されました。これは、cljcファイルとデフォルトのREPLでのみサポートされています。リーダー条件付きは、必要最低限に抑えて使用してください。

リーダー条件付きは、#?または#?@で始まる新しいリーダーディスパッチフォームです。どちらも、condと同様に、一連の交互の特徴と式で構成されます。すべてのClojureプラットフォームには、よく知られた「プラットフォーム機能」があります - :clj:cljs:cljr。リーダー条件付きの各条件は、プラットフォーム機能に一致する機能が見つかるまで順番にチェックされます。リーダー条件付きは、その機能の式を読み取り、返します。選択されていない各ブランチの式は読み取られますが、スキップされます。よく知られた:default機能は常に一致し、デフォルトを提供するために使用できます。一致するブランチがない場合、フォームは読み取られません(リーダー条件付き式が存在しない場合と同じです)。

非公式のClojureプラットフォームの実装者は、名前の衝突を避けるために、プラットフォーム機能に修飾されたキーワードを使用する必要があります。修飾されていないプラットフォーム機能は、公式プラットフォーム用に予約されています。

次の例は、ClojureではDouble/NaN、ClojureScriptではjs/NaN、その他のプラットフォームではnilとして読み取られます。

#?(:clj     Double/NaN
   :cljs    js/NaN
   :default nil)

#?@の構文はまったく同じですが、式は、シンタックスクォートのアンクォートスプライシングと同様に、周囲のコンテキストにスプライスできるコレクションを返すことが期待されます。トップレベルでのリーダー条件付きスプライシングの使用はサポートされておらず、例外がスローされます。例:

[1 2 #?@(:clj [3 4] :cljs [5 6])]
;; in clj =>        [1 2 3 4]
;; in cljs =>       [1 2 5 6]
;; anywhere else => [1 2]

read関数とread-string関数は、オプションのマップを最初の引数としてオプションで受け取ります。現在の機能セットとリーダー条件付きの動作は、これらのキーと値を使用してオプションマップに設定できます。

  :read-cond - :allow to process reader conditionals, or
               :preserve to keep all branches
  :features - persistent set of feature keywords that are active

ClojureからClojureScriptのリーダー条件付きをテストする方法の例:

(read-string
  {:read-cond :allow
   :features #{:cljs}}
  "#?(:cljs :works! :default :boo)")
;; :works!

ただし、Clojureリーダーは常にプラットフォーム機能:cljも挿入することに注意してください。プラットフォームに依存しない読み取りについては、tools.readerを参照してください。

リーダーが{:read-cond :preserve}で呼び出された場合、リーダー条件付きと実行されていないブランチは、データとして返されるフォームに保持されます。リーダー条件付きは、:form:splicing?フラグのキーでキーワード取得をサポートする型として返されます。読み取られたがスキップされたタグ付きリテラルは、:form:tagキーのキーワード取得をサポートする型として返されます。

(read-string
  {:read-cond :preserve}
  "[1 2 #?@(:clj [3 4] :cljs [5 6])]")
;; [1 2 #?@(:clj [3 4] :cljs [5 6])]

次の関数も、これらの型の述語またはコンストラクターとして使用できます。
reader-conditional? reader-conditional tagged-literal? tagged-literal