Help us understand the problem. What is going on with this article?

Julia で定義済の変数を"名前 (`Symbol`)"で取得する方法

More than 1 year has passed since last update.

初めに

Julia でメタプログラミングやってると「なんで isdefined() は存在するのに名前指定で変数を取得する関数がないのだ?」って思いますよね?1

ということでそういう関数を書いてみました。

【2017/08/16】もっとシンプルな方法があったので大幅に加筆修正しました><

先に成果物

こんな関数作りました(対象バージョン:Julia v0.5.2/v0.6.0):

getbyname.jl
@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) # => Int64getbyname(:π) # => π = 3.1415926535897... など
  • getbyname(«module», «symbol») で、そのモジュールで定義されている(または import している等の理由で参照可能な)変数・定数・関数・型等を(存在すれば)を返す。
    • Base[:exp] のように Dictionary のキーを指定して値を返すのと同じ記法で書けるエイリアスも用意(この場合返ってくるのは exp() 関数ですね)。
  • getbyname(«object», «symbol») で、そのオブジェクトに存在するメンバを返す。

要するに isdefined([△,] ○) で true が返ってくるものは getbyname([△,] ○) でその実体が取得できる、ということ2

具体例:

getbyname_sample.jl
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. 普通には参照できない変数等を参照したい

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)。

getbyname_20170813.jl
@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() を使うコードはその小さな関数の中だけにして、しかも引数は型アノテーションを付けて ModuleSymbol しか受け付けないようにしています。これで最低限の安全は保証できる、と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、その分パフォーマンスは十分考慮されている、ということですね。

参考


  1. まぁそもそもやってる人少ないとは思いますが(^-^; 

  2. 実際には isdefined() には第2引数に Int つまりインデックス番号を受け取る使い方もあるのですが、getbyname() はそれには対応していません。それは「名前」じゃないですしそれで true が返ってくるようなものなら普通に △[○] でその値が取得できるように実装されているはずなので。 

  3. もちろん実行する度に結果(主に最後の整数値(=実はカウンタ値))は異なります。 

  4. さらに安全にするために、毎回 eval() するのではなく、第1引数が Module じゃない場合と同様、@generated function にする(というか _getbyname(o, ::Typa{Val{T}}) をそのまま利用)という方法もあったのですが、毎回 eval() した方が圧倒的にパフォーマンス良かった(10倍くらい速かった)ので現在の形になりましたこの形にしていました…が getfield() というもっと良い方法が見つかった今となっては…。 

  5. 実際(前にもちらっと言及しましたが)、Julia のコード a.b は、評価時に getfield(a, :b) に一旦置き換えられてからJITコンパイルが走る仕組みになっているようです。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした