Clojure

事前コンパイルとクラス生成

ClojureはロードされたすべてのコードをJVMバイトコードにオンザフライでコンパイルしますが、事前コンパイル(AOT)が有利な場合があります。AOTコンパイルを使用する理由としては、以下のものがあります。

  • ソースコードなしでアプリケーションを提供するため

  • アプリケーションの起動を高速化するため

  • Javaで使用するために名前付きクラスを生成するため

  • ランタイムバイトコード生成とカスタムクラスローダーを必要としないアプリケーションを作成するため

Clojureのコンパイルモデルは、Javaのコードリロードの制限にもかかわらず、Clojureの動的な性質を可能な限り維持します。

  • ソースとクラスファイルのパシングは、Javaクラスパスの規則に従います。

  • コンパイルの対象は名前空間です。

  • 各ファイル、関数、およびgen-classは.classファイルを生成します。

  • 各ファイルは、同じ名前に"__init"が付加されたローダークラスを生成します。

  • ローダークラスの静的イニシャライザは、ソースファイルをロードした場合と同じ効果をもたらします。

    • 通常、これらのクラスを直接使用する必要はありません。use、require、およびloadは、これらのクラスとより最近のソースの中から選択するためです。

  • ローダークラスは、名前空間がコンパイルされるときに、ローダー.classファイルがソースよりも古い場合に、参照される各ファイルに対して生成されます。

  • スタンドアロンのgen-class機能は、Javaクラスとして直接使用するための名前付きクラスを作成するために提供され、以下の機能を備えています。

    • 生成されるクラスの名前付け

    • スーパークラスの選択

    • 実装するインターフェースの指定

    • コンストラクタシグネチャの指定

    • 状態の指定

    • 追加メソッドの宣言

    • 静的ファクトリメソッドの生成

    • mainの生成

    • 実装する名前空間へのマッピングの制御

    • 継承されたprotectedメンバーの公開

    • 1つ以上の名前空間で実装された、単一のファイルから複数の名前付きクラスを生成

  • オプションの:gen-classディレクティブは、ns宣言で使用して、名前空間に対応する名前付きクラスを生成できます。(:gen-class …​)が指定されている場合、デフォルトでは、:nameはns名に対応し、:mainはtrue、:impl-nsはnsと同じ、:init-impl-nsはtrueになります。gen-classのすべてのオプションがサポートされています。

  • gen-classと:gen-classディレクティブは、コンパイルされていない場合は無視されます。

  • スタンドアロンのgen-interface機能は、Javaインターフェースとして直接使用するための名前付きインターフェースクラスを生成するために提供され、以下の機能を備えています。

    • 生成されるインターフェースの名前付け

    • スーパーインターフェースの指定

    • インターフェースメソッドのシグネチャの宣言

コンパイル

ライブラリをコンパイルするには、compile関数を使用し、名前空間名をシンボルとして指定します。クラスパスにあるmy/domain/lib.cljで定義された名前空間my.domain.libの場合、以下が発生します。

  • ローダークラスファイルは、クラスパスにある*compile-path*の下のmy/domain/lib__init.classに生成されます。

  • 名前空間内の関数ごとに、my/domain/lib$fnname__1234.classなどの名前を持つクラスファイルのセットが生成されます。

  • 各gen-classについて

    • スタブクラスファイルが指定された名前で生成されます。

コンパイラオプション

Clojureコンパイラは、いくつかのコンパイラフラグを使用して制御できます。実行時には、これらは動的変数clojure.core/*compiler-options*に格納されます。これは、以下のオプションのキーワードキーを持つマップです。

  • :disable-locals-clearing (boolean)

  • :elide-meta (キーワードのベクター)

  • :direct-linking (boolean)

これらのコンパイラオプションは、compile関数への呼び出しを囲む動的バインディングで変更して、コンパイラの動作を変更できます。

あるいは、起動時にJavaシステムプロパティを介してコンパイラオプションを設定することもできます。

  • -Dclojure.compiler.disable-locals-clearing=true

  • "-Dclojure.compiler.elide-meta=[:doc :file :line :added]"

  • -Dclojure.compiler.direct-linking=true

これらの各オプションの詳細については、以下を参照してください。

ローカルクリアリング

デフォルトでは、Clojureコンパイラは、ローカルバインディングへのGC参照を積極的にクリアするコードを生成します。ただし、デバッガを使用すると、ローカルはnullとして表示され、デバッグが困難になります。disable-locals-clearing=trueを設定すると、ローカルのクリアが防止されます。本番環境のコンパイルでは、ローカルのクリアを無効にすることはお勧めしません。

メタデータの省略

変数メタデータ(docstring、ファイルと行の情報など)は、コンパイルされたクラスの定数プールに文字列としてコンパイルされます。クラスサイズを小さくし、クラスのロードを高速化するために、メタデータを省略できます。このオプションは、削除するメタデータキーワードのベクターを取ります。一般的なものには、:doc:file:line:addedなどがあります。メタデータを省略すると、特定の機能が動作しなくなる可能性があることに注意してください(たとえば、docstringが省略されている場合、docはdocstringを返すことができません)。

ダイレクトリンク

通常、関数を呼び出すと、変数が逆参照されて関数を実装する関数インスタンスが検索され、その関数が呼び出されます。変数を介したこの間接参照は、Clojureが動的ランタイム環境を提供する方法の1つです。ただし、本番環境での関数呼び出しの大部分は、この方法で再定義されることはなく、不要なリダイレクトが発生することが長い間観察されてきました。

*ダイレクトリンク*を使用すると、この間接参照を関数の直接静的呼び出しに置き換えることができます。これにより、変数呼び出しが高速化されます。さらに、コンパイラは未使用の変数をクラスの初期化から削除でき、ダイレクトリンクにより、さらに多くの変数が未使用になります。通常、これにより、クラスサイズが小さくなり、起動時間が短縮されます。

ダイレクトリンクの1つの結果は、ダイレクトリンクでコンパイルされたコードでは、変数の再定義が表示されないことです(ダイレクトリンクでは変数の逆参照が回避されるため)。^:dynamicとしてマークされた変数は、ダイレクトリンクされることはありません。再定義をサポートするものとして変数をマークする場合(ただし動的ではない)、ダイレクトリンクを回避するために^:redefでマークします。

Clojure 1.8以降、Clojureコアライブラリ自体はダイレクトリンクでコンパイルされています。

ランタイム

Clojureによって生成されたクラスは非常に動的です。特に、gen-classではメソッド本体やその他の実装詳細は指定されていないことに注意してください。gen-classはシグネチャのみを指定し、生成するクラスはスタブのみです。このスタブクラスは、すべての実装を実装名前空間で定義された関数に委任します。実行時に、生成されたクラスのメソッドfooを呼び出すと、実装名前空間/prefixfooを実装する変数の現在の値が検索され、呼び出されます。変数がバインドされていないかnilの場合、スーパークラスメソッドを呼び出すか、インターフェースメソッドの場合はUnsupportedOperationExceptionを生成します。

gen-classの例

最も単純なケースでは、空の:gen-classが提供され、コンパイルされたクラスにはmainのみが含まれ、これは名前空間で-mainを定義することによって実装されます。ファイルはsrc/clojure/examples/hello.cljに保存する必要があります。

(ns clojure.examples.hello
    (:gen-class))

(defn -main
  [greetee]
  (println (str "Hello " greetee "!")))

コンパイルするには、ターゲット出力ディレクトリ`classes`が存在することを確認してください。

mkdir classes

クラスパスを記述するdeps.ednファイルを作成します。

{:paths ["src" "classes"]}

次に、次のようにコンパイルしてクラスを生成します。

$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.hello)
clojure.examples.hello

そして、通常のJavaアプリケーションのように実行できます(出力クラスディレクトリを含めるようにしてください)。

java -cp `clj -Spath` clojure.examples.hello Fred
Hello Fred!

より複雑な:gen-classと、gen-classおよびgen-interfaceへのスタンドアロン呼び出しの両方を使用する例を次に示します。この場合、インスタンスを作成するクラスを作成しています。clojure.examples.instanceクラスはjava.util.Iteratorを実装します。これは、実装がステートフルである必要があるという点で、特に厄介なインターフェースです。このクラスは、コンストラクタで文字列を受け取り、文字列から文字を配信するという点でIteratorインターフェースを実装します。:init句はコンストラクタ関数を指定します。:constructors句は、コンストラクタシグネチャからスーパークラスコンストラクタシグネチャへのマップです。この場合、スーパークラスはデフォルトでObjectになり、そのコンストラクタは引数を取りません。このオブジェクトはstateと呼ばれる状態と、テストできるようにmainを持ちます。

:init関数(この場合は-init)は、常にベクターを返すという点で ungewöhnlich です。ベクターの最初の要素はスーパークラスコンストラクタの引数のベクターです。スーパークラスは引数を取らないため、このベクターは空です。ベクターの2番目の要素はインスタンスの状態です。状態を変更する必要があるため(そして状態は常にfinalです)、文字列と現在のインデックスを含むマップへの参照を使用します。

hasNextとnextは、Iteratorインターフェースのメソッドの実装です。メソッドは引数を取りませんが、インスタンスメソッドの実装関数は常に、メソッドが呼び出されるオブジェクトに対応する追加の最初の引数を取ります。これは、ここでは慣例により「this」と呼ばれます。通常のJavaフィールドアクセスを使用して状態を取得する方法に注意してください。

gen-interface呼び出しは、単一のメソッドbarを持つclojure.examples.IBarと呼ばれるインターフェースを作成します。

スタンドアロンのgen-class呼び出しは、実装名前空間が現在の名前空間にデフォルト設定される、clojure.examples.implという別の名前付きクラスを生成します。これはclojure.examples.IBarを実装します。:prefixオプションを使用すると、メソッドの実装は、デフォルトの "-" ではなく "impl-" で始まる関数にバインドされます。:methodsオプションは、スーパークラス/インターフェースに存在しない新しいメソッドfooを定義します。

main関数では、通常のJava相互運用を使用して、クラスのインスタンスを作成し、メソッドを呼び出す方法に注目してください。Javaからの使用も同様に簡単です。

(ns clojure.examples.instance
    (:gen-class
     :implements [java.util.Iterator]
     :init init
     :constructors {[String] []}
     :state state))

(defn -init [s]
  [[] (ref {:s s :index 0})])

(defn -hasNext [this]
  (let [{:keys [s index]} @(.state this)]
    (< index (count s))))

(defn -next [this]
  (let [{:keys [s index]} @(.state this)
        ch (.charAt s index)]
    (dosync (alter (.state this) assoc :index (inc index)))
    ch))

(gen-interface
 :name clojure.examples.IBar
 :methods [[bar [] String]])

(gen-class
 :name clojure.examples.impl
 :implements [clojure.examples.IBar]
 :prefix "impl-"
 :methods [[foo [] String]])

(defn impl-foo [this]
  (str (class this)))

(defn impl-bar [this]
  (str "I " (if (instance? clojure.examples.IBar this)
              "am"
              "am not")
       " an IBar"))

(defn -main [s]
  (let [x (new clojure.examples.instance s)
        y (new clojure.examples.impl)]
    (while (.hasNext x)
      (println (.next x)))
    (println (.foo y))
    (println (.bar y))))

上記のようにコンパイルします。

$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.instance)
clojure.examples.instance

通常のJavaアプリケーションのように実行します。

java -cp `clj -Spath` clojure.examples.instance asdf
a
s
d
f
class clojure.examples.impl
I am an IBar