CLOVER🍀

That was when it all began.

Clojureでコードのロード系関数+evalを扱う

少し前にこういうエントリを書きました。その後、元にしたブログのエントリを見ていると、evalとかそういう方向に流れていて、この時に自分が取った方法は随分と遠回りだったことがわかりました。

つか、evalの発想がなかった…。まだまだ考え方は静的言語ってことですね。

まあ、せっかくなので、コード片とかをロードする関数群とevalを触ってみようと思います。

なお、ファイル読み込み系の関数で使用するスケープゴートとしては、以下のものを用意しています。
foo.clj

(defn bar [] (println "Hello Foo Bar"))

このファイルをカレントディレクトリに置いた状態で、サンプルコードを実行していきます。

load

いきなり、変なのから入ります(笑)。名前からして、ファイルからコードを読み込む関数かと思いきや、そうではなく、引数で指定された文字列に対応するものを、クラスパスから読み込むんだそうです(Clojuredocsより)。読み込まれた内容は、Clojureコードとして評価されます。また、この関数でファイルを読む場合は、拡張子「.clj」を付与してはいけません。

(load "foo")
(bar)  ;; => Hello Foo Bar

拡張子まで含めてしまうと、FileNotFoundExceptionが発生します。

(try
  (load "foo.clj")
  (catch Exception e (println e)))  ;; #<FileNotFoundException java.io.FileNotFoundException: Could not locate foo.clj__init.class or foo.clj.clj on classpath: >

load-file

引数で指定したファイルを読み込み、Clojureコードとして評価して読み込みます。

(load-file "foo.clj")
(bar)  ;; => Hello Foo Bar

この例だと、あんまり面白味はないですかね…。

load-reader

ファイルから読み込むのではなく、Readerから読み込みます。

(use '[clojure.java.io :only (reader)])
(with-open [r (reader "foo.clj" :encoding "UTF-8")]
  (load-reader r))
(bar)  ;; => Hello Foo Bar

読み込まれた内容が、Clojureコードとして評価されるのはloadやload-fileと同じです。

load-string

今度は、文字列をClojureコード片として評価します。

(load-string "(println \"Hello World\")")  ;; => Hello World

…動的言語のeval関数って、普通これですよね?それはさておき、load-stringはload-readerを使って実装されているので、基本的に動作は同じです。

ところで、元々のエントリを書くきっかけになったエントリでは、「関数名だけ」を評価したいってことらしく、以下の実装が動作することを書いていました。

((load-string "println") "Hello World")  ;; => Hello World

これって何が返って来てるん??ってことで。

(println (load-string "println"))  ;; => #<core$println clojure.core$println@3a2b6ce6>

というわけで、core$printlnクラスのインスタンスが取得できていますね。自分が以前にやったことは、これを遠回りに実装したってことになるんでしょうなぁ…。

よって、if特殊形式のような、クラスとしての実体を持たないようなものを取得しようとすると、実行に失敗します。

(try
  (load-string "if")
  (catch Exception e (println e))) ;; => #<CompilerException java.lang.RuntimeException: Unable to resolve symbol: if in this context, compiling:(null:29)>

普通に、Clojureコードとして評価する分には問題ないのですが。

(load-string "(println (if true \"yes\" \"no\"))")  ;; => yes

eval

上記までの関数とは、ちょっと位置付けが異なります。load系の関数は、コード生成のカテゴリに属していますが、evalはフロー制御に属しています。そして、evalという名前の割には文字列を引数に取りません。
evalはデータ構造(要はリスト)を引数に取ります。

つまり、以下のようなコードは意図通りに動作しません。

(println (eval "(println (if true \"yes\" \"no\"))")) ;; => (println (if true "yes" "no"))

渡した文字列そのものが返ってきています(evalの結果をprintlnに渡しているのは、evalしただけだと何も出力されないからです…)。

よって、動作させたければリストの形式で渡してやればよい、ということになります。

(eval '(println (if true "yes" "no"))) ;; => yes

先ほどの

(load-string "println")

のようなことがしたければ、先にシンボルにしてしまえばよいようです。

((eval (symbol "println")) "Hello World") ;; => Hello World
((eval 'println) "Hello World")  ;; => Hello World

この場合、やはりcore$printlnクラスのインスタンスが取得できています。

(println (eval 'println))  ;; => #<core$println clojure.core$println@3a2b6ce6>

文字列からevalしたい、というのであれば、read-stringと組み合わせましょう。

(eval (read-string "(println \"Hello World\")")) ;; => Hello World

(蛇足)
今回はfoo.cljをload系の関数で読み込んでいましたが、useとかで読み込みたければ名前空間を切って使えばよさそう。
hoge.clj

(in-ns 'hoge)
(clojure.core/use 'clojure.core)
(defn bar [] (println "Hello Hoge Bar"))i

利用側。

(use 'hoge)
(bar)

useで指定する名前空間に.cljファイルがいることを想定しているっぽいので、ファイル名と宣言する名前空間を不一致にしてしまうと、動作できなくなる模様。