uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Clojureの型ヒント

遅まきながら、一部でブーム?のClojureの本、「プログラミングClojure」読んでます。

プログラミングClojure

プログラミングClojure

ClojureJVM上のSchemeライクなlisp関数型言語で高速とのことです。

Clojureに興味があったのは、何で高速なのかを知りたかったということが一つです。マイクロなベンチマーク(1,2)を見るとなかなか速いらしい(少なくともGroovyよりは)。Lispも動的系の言語なのに、なぜその差が生じるのかなと。

まだ本を途中まで読んだ限りですが、高速であることの理由として、関数型だから副作用が無くてロックしないコレクションを持ってるとかもあると思うわけですが(まだそこまで読んでない)そういう高尚なところを除いて、一つわかったことを書いときます。

Clojureの場合、オプショナルタイピングじゃないけど、「型ヒント」なるものを関数の引数やローカル変数などに指定することができます。たとえば

(defn hogehoge [#^String str]
..
(.toUpperCase str ))

みたいに書けて、赤字の「#^String」がメタ情報としての型ヒント指定なのですが、これは引数strの型がjava.lang.Stringであることを示します。型ヒントをつけない場合は

(defn hogehoge [str] 
  ..
  (.toUpperCase str))

こうね。ちなみに引数を[..]で指定するのはClojure流で、字句的にはベクターリテラルだそうです。

話を戻して、型ヒントをつけないと上の場合toUpperCaseはリフレクションをつかって呼び出されるけど、型ヒントをつけると、

  ((String)str).toUpperCase()

みたいな型キャストを使ったコードが生成される。Javaと同等レベルの効率になるわけです。

メソッド呼び出しについて、リフレクションを使った呼び出しか、いわゆるvtableを使った呼び出しか、ということを切り替えられるということです。後者の方がもちろん速い。

さらに面白いのは、Clojureの場合、受け取った引数を別のメソッドの引数に渡したりする場合には、型チェックがスルーされるということです。
例えば、以下のコード

(defn hogehoge [^#String str]
   (println str))

のように、strのメソッドを呼び出さずに、他のメソッドにstrを受け渡すだけの場合、hogehogeに対してString型ではないオブジェクトを渡しても問題なく動くとのこと。おそらくバイトコードのコンパイル結果としてはJava

void hogehoge(String str) {
  System.out.println(str);
}

のように書いたのと同じように引数を「String」にコンパイルしているわけではなく、

void hogehoge(Object str) {
System.out.println(str);
}

のようにObject型で受け渡していると想像されます*1。その意味で、静的型では無くて、動的な強い型制約でもなくて、あくまで最適化・高速化のためのヒントでしかない*2

本当にそうかどうかは自信が無いのですが、Clojureが速いのは、ひとことでいうと動的型言語ではあっても動的言語ではないということなのかな?(Clojureメタ機能の有無や度合いについてはまだ読み終わってないので不明)。

Groovyのオプショナルタイピングとも似てるのですが、Groovyではメソッド呼び出し時に引数の型チェックが実行されます。なので

void func(String s){println s}
func(3)

というような呼び出しはエラーになります。また単なるキャストになるわけでもありません(なので実行速度向上にはあんまり貢献しない)。

あとは余談ですが、上の型ヒントは「メタ情報」なのですが、一般に、Clojureでは変数にメタ情報というものを紐付けられます。メタ情報には他に、Emacs Lispみたいなドキュメントコメントとか、「テストケース」があります。関数にその関数のテストケースを紐付けることもできるというわけです。アノテーションみたいなもんだと思いますが、応用の方向が面白いですね。

*1:その意味では、Javaと比しての動的なペナルティはありうる。キャスト実行命令が実行されるから。ただ、JITコンパイルでそのうち僅少化されると思うけど。

*2:まーこりゃコンパイル系Lispではあたりまえだのクラッカーな話ですけども