CLOVER🍀

That was when it all began.

Clojureの関数を、文字列での名前から動的に呼び出す

面白そうなチャレンジを見つけたので、別のアプローチでやってみることにしました。
*記事を執筆後、ちょっと書き直しました
http://npnl.hatenablog.jp/entry/2012/06/02/150513

元の記事は、文字列からシンボルを作って、それを評価しようとして四苦八苦されているようです。この発想はなかったですね…。ここから、関数呼び出しができるようになるのかな…?この辺りは、ちょっとよくわかりません。

ところで、Clojure本の10ページ目によると、Clojureの関数はすべてCallableでありRunnableであると書かれています。

プログラミングClojure

プログラミングClojure

よって、Clojureの関数はあるクラスのインスタンスだということが推測できます。

これを確認するために、以下のようなコードを用意してみました。

(defn func
  []
  (println "Hello World!"))

(defn func2
  [arg]
  (println (str "Hello" " " arg "!")))

(println (class func))
(println (class func2))

これを実行してみると、以下のような結果が得られます。

user$func
user$func2

名前空間を付けた場合は?

(in-ns 'myapp.ns)
(clojure.core/use 'clojure.core)

(defn ns-func [] (println "Hello World"))
(println (class ns-func))

こうなりました。

myapp.ns$ns_func

どうやら、「名前空間$関数名」でクラス名となりそうですね。名前空間は、ネストするとパッケージ名になるっぽい??
それと、よく見ると「-(ハイフン)」が「_(アンダースコア)」にすり変わっています。この辺りは、Javaの制限に合わせた名前変換が働くのでしょう。

では、続いて以下のようなことを試してみます。

(doall
 (for [m (.getMethods (class func))]
   (println m)))

結果は、こんな感じ。

#<Method public java.lang.Object user$func.invoke()>
#<Method public int clojure.lang.AFunction.compare(java.lang.Object,java.lang.Object)>
#<Method public clojure.lang.IObj clojure.lang.AFunction.withMeta(clojure.lang.IPersistentMap)>
#<Method public clojure.lang.IPersistentMap clojure.lang.AFunction.meta()>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object[])>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public java.lang.Object clojure.lang.AFn.invoke(java.lang.Object,java.lang.Object,java.lang.Object)>
#<Method public void clojure.lang.AFn.run()>
#<Method public java.lang.Object clojure.lang.AFn.call()>
#<Method public java.lang.Object clojure.lang.AFn.applyTo(clojure.lang.ISeq)>
#<Method public static java.lang.Object clojure.lang.AFn.applyToHelper(clojure.lang.IFn,clojure.lang.ISeq)>
#<Method public java.lang.Object clojure.lang.AFn.throwArity(int)>
#<Method public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException>
#<Method public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException>
#<Method public final void java.lang.Object.wait() throws java.lang.InterruptedException>
#<Method public boolean java.lang.Object.equals(java.lang.Object)>
#<Method public java.lang.String java.lang.Object.toString()>
#<Method public native int java.lang.Object.hashCode()>
#<Method public final native java.lang.Class java.lang.Object.getClass()>
#<Method public final native void java.lang.Object.notify()>
#<Method public final native void java.lang.Object.notifyAll()>

なお、func2に対して実行すると、最初のメソッドが

#<Method public java.lang.Object user$func2.invoke(java.lang.Object)>

となります。よって、Clojureの関数呼び出しはinvokeメソッドの構文糖衣な気がしますね。

試してみましょう。

(func)
(func2 "Clojure")
(.invoke func)
(.invoke func2 "Clojure")

結果はこちら。

Hello World!
Hello Clojure!
Hello World!
Hello Clojure!

どうやら、正しそうですね。

そして、ある名前空間で定義された関数は、ns-resolve関数で取得できるようです。

というわけで、こんな関数を用意してみました。

(defn dyna-invoke
  ([namespace func-name]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name))))
  ([namespace func-name arg]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg))
  ([namespace func-name arg1 arg2]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2))
  ([namespace func-name arg1 arg2 arg3]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2 arg3))
  ([namespace func-name arg1 arg2 arg3 args4]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2 arg3 args4)))

the-ns関数でシンボルからNamespaceクラスのインスタンスを取得し、それが関数であることを前提にinvokeメソッドを呼び出します。

先ほどClojureの関数からメソッドをダンプした時にえらくinvokeが大量に現れましたが、Clojureの関数は引数の数だけ定義されているようなので可変長引数でごまかすことはできなさそうです。今回は、4つまで作成しました。

では、使ってみます。

(defn func
  []
  (println "Hello World!"))

(defn func2
  [arg]
  (println (str "Hello" " " arg "!")))

(dyna-invoke 'user "func")
(dyna-invoke 'user "func2" "Clojure")

(in-ns 'myapp.ns)
(clojure.core/use 'clojure.core)

(defn ns-func [s] (println (str "Hello" " " s "!")))
(user/dyna-invoke 'myapp.ns "ns-func" "Namespace Function")

結果は、こんな感じ。

$ clj func_test.clj 
Hello World!
Hello Clojure!
Hello Namespace Function!

clojure.coreの関数でも大丈夫です。

(user/dyna-invoke 'clojure.core "println" "Hello World")

結果。

Hello World

ただ、ifは関数ではないため、この機構だけでは残念ながら以下のようなことはできません。

(user/dyna-invoke 'clojure.core "if" true "yes" "no")

これだと

Exception in thread "main" java.lang.NullPointerException
	at clojure.lang.Reflector.invokeInstanceMethod(Reflector.java:26)
	at user$dyna_invoke.invoke(func_namespace.clj:9)
	at myapp.ns$eval17.invoke(func_namespace.clj:30)
	at clojure.lang.Compiler.eval(Compiler.java:6511)
	at clojure.lang.Compiler.load(Compiler.java:6952)
	at clojure.lang.Compiler.loadFile(Compiler.java:6912)
	at clojure.main$load_script.invoke(main.clj:283)
	at clojure.main$script_opt.invoke(main.clj:343)
	at clojure.main$main.doInvoke(main.clj:427)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.lang.Var.invoke(Var.java:415)
	at clojure.lang.AFn.applyToHelper(AFn.java:161)
	at clojure.lang.Var.applyTo(Var.java:532)
	at clojure.main.main(main.java:37)

となってしまいます…。
これをやりたい場合には、特殊形式を呼べるようにもうちょっと努力が必要なのでしょうね。

今回作成したコードはこちらです。

(defn dyna-invoke
  ([namespace func-name]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name))))
  ([namespace func-name arg]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg))
  ([namespace func-name arg1 arg2]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2))
  ([namespace func-name arg1 arg2 arg3]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2 arg3))
  ([namespace func-name arg1 arg2 arg3 args4]
     (.invoke (ns-resolve (the-ns namespace) (symbol func-name)) arg1 arg2 arg3 args4)))

(defn func
  []
  (println "Hello World!"))

(defn func2
  [arg]
  (println (str "Hello" " " arg "!")))

(dyna-invoke 'user "func")
(dyna-invoke 'user "func2" "Clojure")

(in-ns 'myapp.ns)
(clojure.core/use 'clojure.core)

(defn ns-func [s] (println (str "Hello" " " s "!")))
(user/dyna-invoke 'myapp.ns "ns-func" "Namespace Function")
(user/dyna-invoke 'clojure.core "println" "Hello World")
;; (user/dyna-invoke 'clojure.core "if" true "yes" "no")