Clojureは動的型付け言語です。
つまり、変数や関数の戻り値、引数の型宣言はありません。
(defn hoge [s] (.toUpperCase s))
でも、結局のところ、引数に渡すオブジェクトや関数の戻り値で使っているのはJavaのクラスのオブジェクトだったりするわけです。
さて、ではどのようにして型を解決するのか。
Clojureはリフレクションで解決しているみたいです。
うーん、これは微妙な時もありますよね。できればちゃんとキャストで解決してほしい、そういうこともあると思いますし、やっぱり多用すると効率がぐんぐん下がってしまうこともあると思います。
そこでリーダメタデータというもののうち、:tagキーを用いることでキャストに対するヒントを与えてやることができるようです。
ちなみに強い型制約が与えられるわけではないみたいです。そういうことを期待した人にはここに解答はないのでごめんなさい。
さて、早速リーダメタデータを使ってみます。先ほどのサンプルコードに対して、引数、戻り値ともにStringであるということをメタデータとして付加してみます。ちなみにデータ自体に付加されるというよりは、コンパイラのためにコードに付加されるメタデータのことをリーダメタデータというみたいです。リーダですから。
(defn #^{:tag String} hoge-tag [#^{:tag String} s] (.toUpperCase s))
これで戻り値がStringかつ引数もStringであることを明示できました。実際にメタデータとして付加されているかを確認するためにはmeta関数を用いれば確認できます。REPL上で試してみるといいと思います。
user=> (meta #'hoge-tag) {:ns #<Namespace user>, :name hoge-tag, :file "NO_SOURCE_PATH", :line 10, :arglists ([s]), :tag java.lang.String}
:tagキー以外にもClojureによって付加されたキーがあります。他のキーがどんな情報を保持しているかは大体想像がつくと思います。ところで、:tagはちゃんと与えた情報を保持しているでしょうか。java.lang.Stringとなっていますから、保持しているみたいです。
さて、実際に使ってみると今までとは違うことが分かります。たとえば、最初に示したリーダメタデータを付加していないhoge関数を使ってみます。
user=> (hoge "naoki_rin") "NAOKI_RIN" user=> (hoge 123) java.lang.IllegalArgumentException: No matching field found: toUpperCase for class java.lang.Integer(NO_SOURCE_FILE:0)
きちんと例外が発生しています。これは悪くないんじゃない?
でも、どうも関数内部で用いたtoUpperCaseの引数にIntegerが渡せなかったという例外みたいです。
ここで、:tagキーを付加したhoge-tag関数を使ってみます。何が違うのでしょうか。
user=> (hoge-tag "naoki_rin") "NAOKI_RIN" user=> (hoge-tag 123) java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String (NO_SOURCE_FILE:0)
アレ?あんまり変わってないみたいに見えますね。
でもよく見てみると、発生している例外が違います。
今度はキャストに対しての例外になっています。つまりはStringにキャストできずに例外が発生しているみたいです。
これが違いみたいです。
ところで、いちいちリーダメタデータの:tagキーを付加するようにするという宣言は長いですよね。
:tagキーはよく用いられるため、もうすこし簡略化して書けるようになっています。
(defn #^String hoge-tag [#^String s] (.toUpperCase s))
上のように
#^ClassName
のような形で書くことで:tagキーに結びつけて、ちゃんとメタデータが付加されます。
まあ、結論からいえば基本的には効率のアップ程度でしかないのですが、キャストが実際に明示的に内部で行われるために発生する例外の種類が変わるということは忘れてはいけないでしょうね。