初めに
Julia でメタプログラミングやってると「なんで isdefined()
は存在するのに名前指定で変数を取得する関数がないのだ?」って思いますよね?1
ということでそういう関数を書いてみました。
【2017/08/16】もっとシンプルな方法があったので大幅に加筆修正しました><
先に成果物
こんな関数作りました(対象バージョン:Julia v0.5.2/v0.6.0):
@inline getbyname(name::Symbol) = getfield(current_module(), name)
@inline getbyname(o, name::Symbol) = getfield(o, name)
@inline Base.getindex(m::Module, name::Symbol) = getbyname(m, name)
主な使い方:
-
getbyname(«symbol»)
で、引数に渡した名前(Symbol
)の変数・定数・関数・型等が存在したらその実体(値)を返す(なければUndefVarError: «symbol» not defined
のようなエラーになります)。- 例えば
getbyname(:Int) # => Int64
、getbyname(:π) # => π = 3.1415926535897...
など
- 例えば
-
getbyname(«module», «symbol»)
で、そのモジュールで定義されている(またはimport
している等の理由で参照可能な)変数・定数・関数・型等を(存在すれば)を返す。-
Base[:exp]
のようにDictionary
のキーを指定して値を返すのと同じ記法で書けるエイリアスも用意(この場合返ってくるのはexp()
関数ですね)。
-
-
getbyname(«object», «symbol»)
で、そのオブジェクトに存在するメンバを返す。
要するに isdefined([△,] ○)
で true が返ってくるものは getbyname([△,] ○)
でその実体が取得できる、ということ2。
具体例:
include("getbyname.jl")
@show methods(getbyname)
# => # 2 methods for generic function "getbyname":
# getbyname(name::Symbol) at …
# getbyname(o, name::Symbol) at …
@show isdefined(:Int)
# => true
@show getbyname(:Int)
# => Int64
@show isdefined(:undef_var)
# => false
try
@show getbyname(:undef_var)
# Nothing to be shown.
catch ex
@show ex
# => ex = UndefVarError(:undef_var)
end
@show getbyname(big"1", :d)
# => Ptr{UInt64} @0x00007fd8d3235900
@show [getbyname(name) for name=names(current_module()) if ismatch(r"Main", string(name))]
# => Module[Main]
@show [Base[name] for name=names(Base) if ismatch(r"[Nn]ame", string(name))]
# => Function[basename, dirname, fieldname, fieldnames, fullname, gethostname, getsockname, module_name, names, tempname]
【追記】解説
はい、新しい関数定義するまでもありませんでした><
getfield()
という関数がありました。
ドキュメントには「(複合)型の値の、名前で指定されたフィールドを抽出する」というような説明しかありませんが、第1引数がオブジェクトだけでなくモジュールでも使えて、その場合そのモジュールに定義されている(もしくはとにかく M.v
の形式(M
はモジュール名)で参照できる)変数・関数・型等を取得できます。
というよりドキュメントの後半の説明がすべてですね。「a.b
という文法は getfield(a, :b)
を呼んでいます」もうこれに尽きる。
ただ、isdefined(«symbol»)
という引数1個に対応する直接的なものはない(対応するのは getfield(current_module(), «symbol»)
し、関数名も「フィールドを取得する」なので、「(モジュールに定義された)変数等を『名前』で取得する」という別関数(というかほぼエイリアス)として用意することは決して無意味ではないのではないか、と思います(と自分に言い聞かせています><)。
【追記ここまで】
使い途
これだけ見ると「これ何に使うの?」「名前が分かっていれば初めから Int
のように書けば良いし、存在するかどうかは isdefined(:name)
で確認できるし、意味あるの?」等の疑問が湧くかもしれません。
なので、「なぜ私がこんなもの書いたのか」という理由・動機の説明を兼ねて。
主な「使い途」は以下の2つ。
- 動的に変数等を参照したい
- 普通には参照できない変数等を参照したい
1. 動的に変数等を参照したい
元々の動機というのがこれ。
例えば先の具体例コードの最後の2つ。names(m::Module)
関数は、そのモジュールに定義されている変数(関数・型 等)を名前(Symbol
)の一覧で出す関数。
それがあるのに、その名前を指定してその変数(関数・型 等)の実体を返す関数が、ドキュメントのどこを見ても見当たらない。それって対称性がないというか、片手落ちというか。
ま、結果的には「getfield()
関数を使えばその要求を満たすコトが出来る」ということは分かったのですが、ドキュメントを隅から隅まで見ないと気付かない(見ていても気付かない)なんて、…。
【一部修正後のコードに合わせて内容修正】
2. 普通には参照できない変数等を参照したい
以下のコードを見てください:
julia> new_var = gensym(:v)
Symbol("##v#274")
julia> eval(:($new_var = 123))
123
julia> getbyname(new_var)
123
まず最初に new_var = gensym(:v)
で新しい変数名(Symbol
)を生成しています。gensym()
という関数は、今までに生成されているどの Symbol
ともかぶらない一意な新しい Symbol
を自動生成する関数です。マクロでコードを自動生成する場合などによく利用します。
で、その新しい変数に値を代入するには、quote
されたコード(コードブロック)中で $new_var
のように変数名を参照して式を記述し、それを eval()
します。REPL で簡単に書いたのが↑のコードの2つめの式。これで new_var
(が保持する Symbol
) を名前に持つ変数ができました。
この変数に、どうやってアクセスすれば良いのでしょう?
自動生成された名前は ##v#274
のようなもの3になっており、これをそのままコード中に書くと #
がコメントの始まりと解釈されて参照できません。
まぁ普通はこういう自動生成変数はそのマクロ内(等)で完結する用途でしか使わず、後で外から参照することはほぼない(というかないような設計にすべき)なのですが、手段としては用意しておくのは悪くないのではないかな、と。
【追記・修正】修正前のコードとの比較
記事修正する前の成果物として、以下のようなコードをあげていました(対象バージョン:v0.6.0)。
@inline getbyname(name::Symbol) = getbyname(current_module(), name)
@inline getbyname(m::Module, name::Symbol) = eval(m, name)
@inline Base.getindex(m::Module, name::Symbol) = getbyname(m, name)
getbyname(o, name::Symbol) = _getbyname(o, Val{name})
# for VERSION < v"0.6-": @generated function _getbyname{T}(o, ::Type{Val{T}})
@generated function _getbyname(o, ::Type{Val{T}}) where T
:(o.$T)
end
コメント中にあるように、@generated function _getbyname(o, ::Type{Val{T}}) where T
の行を @generated function _getbyname{T}(o, ::Type{Val{T}})
に書き換えれば v0.5.x 以前でも動作します(v0.5.2 で動作確認済)。
こちらの getbyname()
のコード例にもあるように eval(m, name)
とすれば、「そのモジュールにあるその名前(Symbol
)の変数(等)」を取得できます。
でも eval()
って、やっぱ動的すぎて危険ですよね。(第2)引数が Symbol
であれば、ASTの解釈と言ってもたった一つの識別子(変数)の解釈だけなので安全と言えば安全ですが、できればあまり表だって使いたくない。
なので、関数化した、というわけ。eval()
を使うコードはその小さな関数の中だけにして、しかも引数は型アノテーションを付けて Module
と Symbol
しか受け付けないようにしています。これで最低限の安全は保証できる、と4。
あとコードを見ていただくと分かる通り、(モジュール内の)変数を取得するコードは eval()
をそのまま利用していますが、オブジェクト(構造体)のメンバを参照する方は、@generated function
という仕組みを利用しています。
これはパフォーマンス向上目的です。
オブジェクト(構造体)のメンバ参照も、eval()
を使って eval(:(o.$name))
のように書くことも出来ます。ただこれ、eval(name)
に比べるととても遅いのです。
理由は、これは単純な Symbol
の解釈ではなく、まず「o
という変数の name
(という変数に保持された Symbol
値)という名前を持つメンバを参照する」というASTが作られて、それを eval()
で評価する、という流れになっているからです。言ってみれば、「毎回動的にソースコードが生成されてその都度コンパイルが走ってその上で実行される」、ということ。
それに比べて @generated function
と言う仕組みは、eval()
と似たような仕組みなのですが、その評価が行われるのは最初の1回だけ。2回目以降にその関数が呼ばれたときには、そのASTを解釈済のコードが実行されます。先ほどの例との対比で言うと「最初だけソースコードが生成されコンパイルされて実行される、2回目以降はそのコンパイル済のコードが実行される(だから速い)」ということ。
ただしそこに Symbol
を外部からパラメータとして渡すのに一工夫要ります。値型(Val{name}
の部分)という仕組みを利用しています。その辺の詳細は、Julia の公式ドキュメントを眺めてみてください。
ただ、これらの方法より、getfield()
を利用した方がさらにもっとパフォーマンスは良いです。
getfield()
は、C言語レベルで実装されている 組込関数。要するに julia のかなりコアな部分の関数で5、その分パフォーマンスは十分考慮されている、ということですね。
参考
Core.getfield
- Function - Essentials · The Julia Language- Metaprogramming · The Julia Language
- "Value types" - Types · The Julia Language
-
まぁそもそもやってる人少ないとは思いますが(^-^; ↩
-
実際には
isdefined()
には第2引数にInt
つまりインデックス番号を受け取る使い方もあるのですが、getbyname()
はそれには対応していません。それは「名前」じゃないですしそれでtrue
が返ってくるようなものなら普通に△[○]
でその値が取得できるように実装されているはずなので。 ↩ -
もちろん実行する度に結果(主に最後の整数値(=実はカウンタ値))は異なります。 ↩
-
さらに安全にするために、毎回
eval()
するのではなく、第1引数がModule
じゃない場合と同様、@generated function
にする(というか_getbyname(o, ::Typa{Val{T}})
をそのまま利用)という方法もあったのですが、毎回eval()
した方が圧倒的にパフォーマンス良かった(10倍くらい速かった)ので現在の形になりましたこの形にしていました…がgetfield()
というもっと良い方法が見つかった今となっては…。 ↩ -
実際(前にもちらっと言及しましたが)、Julia のコード
a.b
は、評価時にgetfield(a, :b)
に一旦置き換えられてからJITコンパイルが走る仕組みになっているようです。 ↩