Skip to content

Instantly share code, notes, and snippets.

@plaster
Last active February 18, 2017 06:54
Show Gist options
  • Save plaster/4363770 to your computer and use it in GitHub Desktop.
Save plaster/4363770 to your computer and use it in GitHub Desktop.
Clojureのダイナミックスコープと`Var/setDynamic`

https://gist.github.com/4357479 で「ダイナミックスコープな変数であるかどうか」をプログラムから(というかreplから)知る方法について調べた経緯です。

*print-readably* を対象にして調べる途上で出会ったいろいろなものについて、とりとめなくgdgdに書いています。

var

replで *print-readably* だけみると、値については教えてくれますが、変数については教えてくれません。 さっきのしらべものの途中で見つけた http://d.hatena.ne.jp/athos/20111204/elephant_things_in_clojure#'fact の表記があったため、試してみたところ

pe-16.core=> #'*print-readably*
#'clojure.core/*print-readably*

どこの変数なのか教えてくれました。期待がもてそうです。(あとで調べてわかったことですが、#'x(var x) と等価のようです。)

meta:dynamic

話を戻して、たしか meta とかあった気がしたので打ってみたところ

pe-16.core=> (meta #'*print-readably*)
{:ns #<Namespace clojure.core>, :name *print-readably*, :added "1.0", :doc "When set to logical false, strings and characters will be printed with\n  non-alphanumeric characters converted to the appropriate escape sequences.\n\n  Defaults to true"}

いろいろ教えてもらえましたが、:dynamic みたいな属性がありません。

代わりに関数の方には関係のありそうなものが見つかりました。

pe-16.core=> (meta #'pr)
{:ns #<Namespace clojure.core>, :name pr, :arglists ([] [x] [x & more]), :dynamic true, :added "1.0", :doc "Prints the object(s) to the output stream that is the current value\n  of *out*.  Prints the object(s), separated by spaces if there is\n  more than one.  By default, pr and prn print in a way that objects\n  can be read by the reader", :line 3269, :file "clojure/core.clj"}
pe-16.core=> (meta #'print)
{:ns #<Namespace clojure.core>, :name print, :arglists ([& more]), :added "1.0", :static true, :doc "Prints the object(s) to the output stream that is the current value\n  of *out*.  print and println produce output for human consumption.", :line 3316, :file "clojure/core.clj"}

pr の方には :dynamic true が、print の方には :static true がそれぞれあります。もしかしてこれは「挙動がダイナミックスコープの変数に依存している」ことを示す何かなのかもしれないと思ったのですが、先に貼ったprint系関数のドキュメントを見る限り、どちらも出力先が *out* に依存するので、関係ないのかもしれません。 ダメ押し(ダメ押され?)に

pe-16.core=> (meta #'pr-str)
{:ns #<Namespace clojure.core>, :name pr-str, :arglists ([& xs]), :added "1.0", :static true, :doc "pr to a string, returning it", :line 4190, :file "clojure/core.clj", :tag java.lang.String}
pe-16.core=> (meta #'print-str)
{:ns #<Namespace clojure.core>, :name print-str, :arglists ([& xs]), :added "1.0", :static true, :doc "print to a string, returning it", :line 4208, :file "clojure/core.clj", :tag java.lang.String}

どちらも :static true なので、どうやら関係ないのでしょうか?

ちょっと立ち戻って、pr 変数自体がダイナミックスコープなのではと思い当たり、試してみたところ、そのとおりでした。

pe-16.core=> pr
#<core$pr clojure.core$pr@22c190b5>
pe-16.core=> (binding [pr 1] pr)
1
pe-16.core=> pr-str
#<core$pr_str clojure.core$pr_str@1a2bc3>
pe-16.core=> (binding [pr-str 1] pr-str)
IllegalStateException Can't dynamically bind non-dynamic var: clojure.core/pr-str  clojure.lang.Var.pushThreadBindings (Var.java:353)

pr 自体を一時的に置き換えるのは……すごくおもしろそうなのですが、ひとまず置いておきます。

clojure.lang.Var#setDynamic

*print-readably* はダイナミックスコープっぽいのに、どうして:dynamic true がないのでしょう? その答えなのかどうかはわかりませんが、Clojureのソースツリーを 'print-readably' でgrepしたところ、 どうもこの変数はJavaでセットアップしているらしいことがわかりました。 https://github.com/clojure/clojure/blob/79a1b793f87af417b430450f3c24e7cfe456e3e2/src/jvm/clojure/lang/RT.java#L215

名前からしてsetDynamic でダイナミックスコープを指定していると予想されます。とすれば、自分でsetDynamicを呼べば、ダイナミックスコープになるに違いありません。試してみます。

まず、ふつうにdefした場合はbindingできません。

pe-16.core=> (def x 1)
#'pe-16.core/x
pe-16.core=> (binding [x 2] x)
IllegalStateException Can't dynamically bind non-dynamic var: pe-16.core/x  clojure.lang.Var.pushThreadBindings (Var.java:353)

つぎに、RT.java中のコードを真似て setDynamicしてみると

pe-16.core=> (import clojure.lang.Var clojure.lang.Symbol clojure.lang.Namespace)
clojure.lang.Namespace
pe-16.core=> (def pe-16ns (Namespace/findOrCreate (Symbol/intern "pe-16.core")))
#'pe-16.core/pe-16ns
pe-16.core=> (. (Var/intern pe-16ns (Symbol/intern "x") nil) setDynamic)
#'pe-16.core/x
pe-16.core=> (binding [x 2] x)
2

binding が使えました。ダイナミックスコープになってます! ……と言いたいところですが

pe-16.core=> x
nil

元のxの値にアクセスできなくなってしまいました。大きなシステムの中の機能をよくわからないまま単独で叩いているのですから、 挙動が不完全なのは、まあ仕方ありません…?

もしかすると、defで再定義相当のことをしてしまっているのではないでしょうか?

後でソースをもう一度見てわかったのですが: https://github.com/clojure/clojure/blob/79a1b793f87af417b430450f3c24e7cfe456e3e2/src/jvm/clojure/lang/Var.java#L130 3引数の Var/intern は、4引数目の replaceRoottrue を指定したのと同じことになるようです。 やはり、これはいかにも束縛を作りなおしてしまっています。nilになったのは、第3引数のnilがそのまま初期値になったためですね。

というわけで、replを上げなおして、やり直します。

pe-16.core=> (def x 1)
#'pe-16.core/x
pe-16.core=> x
1

普通に定義したあと、bindingが失敗することを再確認します。

pe-16.core=> (binding [x 2] x)
IllegalStateException Can't dynamically bind non-dynamic var: pe-16.core/x  clojure.lang.Var.pushThreadBindings (Var.java:353)

エラーになったからといって、元の値が消えたりはしていません。

pe-16.core=> x
1

さてsetBindingはもっと簡単に呼べそうな気がしたので、試します。

pe-16.core=> (. #'x setDynamic)
#'pe-16.core/x

少なくともエラーにはなっていません。ダイナミックスコープになっているか試します。

pe-16.core=> (binding [x 2] x)
2
pe-16.core=> x
1

やった!

コンパイラの中に迷い込みかけた

RT.javaで*print-readably*を初期化している部分との差異がわかれば謎の解明に近づけそうなので、素潜りしてみることにします。 def^:dynamic する場合もsetDynamicしているに違いない……と予想して、setDynamic でgrepすると、Compiler.java 中にたくさんヒットしました。

それらしい場所はおそらく2箇所だけで、いずれもDefExpr および DefExpr.Parserクラス中にあります。名前からして、おそらくパーサの部品で、かつdefの処理をしている部分に違いありません。 https://github.com/clojure/clojure/blob/79a1b793f87af417b430450f3c24e7cfe456e3e2/src/jvm/clojure/lang/Compiler.java#L412 https://github.com/clojure/clojure/blob/79a1b793f87af417b430450f3c24e7cfe456e3e2/src/jvm/clojure/lang/Compiler.java#L494

結論: clojure.lang.Var#isDynamic

これを追うのもすごく興味深いのですが、ふと「setDynamicがあるならisDynamicもあるのでは?」と思いついたので、それを試してみることにします。 さっきのreplのまま

pe-16.core=> (. #'x isDynamic)
true

元々の目的だった「変数が動的スコープかどうか」が、どうやらこれで分かりそうです!

pe-16.core=> (. #'*print-readably* isDynamic)
true
pe-16.core=> (. #'pr isDynamic)
true
pe-16.core=> (. #'print isDynamic)
false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment