Juliaのシンボルとは?
先ほど何となくstackoverflowのJuliaに関する投稿を見ていたら、なるほど確かに最初は分からないかもしれないなと思う疑問と、それに対する分かりやすいKarpinski氏の回答があったので紹介しようと思います。
シンボル(Symbol
)とは何か
Juliaを使っていると、シンボルというよく分からない型に出会うことがあります。
PythonやRを使っている人にとっては、初めて目にするものかもしれません。
Rubyを使っている人は名前は知っていると思いますが、Juliaではまた違った使い方がされます。
例えば、REPLで:foo
と打って、型を確認するとそいつが出現します。
julia> :foo :foo julia> typeof(:foo) Symbol
この Symbol
とは一体何であって、なんのために使うのでしょうか?
ひとことで言えば、『シンボルはメタプログラミングにおいて変数を表すのに使われる (a symbol is used to represent a variable in metaprogramming)』ものです。
これだけでは分からないので、少し例を出しつつ説明していきましょう。
Juliaは、偉大なプログラミング言語でアイデアの源泉であるLispから多くのものを受け継ぎました。
それは、プログラム自体をプログラムで操作する能力です。
JuliaはプログラムのコードをJuliaのデータ構造として持つことができ、コードを書き換えることができます。
:(...)
もしくはquote ... end
のようにコードをラップ(quote: クォート)することで、そのコード自体を表現するデータ構造を作り出すことができます。
コード x + 1
をクォートすると :(x + 1)
になるといった具合です。
このデータ構造は Expr
と命名されています。
julia> :(x + 1) :(x + 1) julia> typeof(:(x + 1)) Expr
このデータ構造を詳しく見てみると、以下の様な構造を指定ます。
julia> xdump(:(x + 1)) Expr head: Symbol call args: Array(Any,(3,)) 1: Symbol + 2: Symbol x 3: Int64 1 typ: Any::DataType <: Any
ここに、3つのシンボル(Symbol
)が出てきていることに気がつくと思います。
call
については、Juliaが付加したものですが、+
とx
は両方とも元々のコード x + 1
に含まれていたシンボルです。
Expr: - `call` (Symbol) - `+` (Symbol) - `x` (Symbol) - `1` (Int64)
個々のシンボルは、以下のように取り出すこともできます。
julia> ex = :(x + 1) :(x + 1) julia> ex.args[1] :+ julia> ex.args[2] :x julia> ex.args[3] 1
+
や x
といったシンボルは、元々のコード x + 1
の中で変数として使われていたものです (+
のような演算子もJuliaでは他の変数と変わりありません)。
すなわち、シンボルはJuliaのコードの変数を表すのに使われるデータということになります。
そう考えると、Juliaのコードは数値 (42
) や文字列 ("foo"
) などのリテラルやコメントを除くとほとんどシンボルだということがわかると思います。
const
や=
でさえも、Julia内部ではシンボルとして扱われています。
julia> xdump(:(const W = WORD_SIZE)) Expr head: Symbol const args: Array(Any,(1,)) 1: Expr head: Symbol = args: Array(Any,(2,)) 1: Symbol W 2: Symbol WORD_SIZE typ: Any::DataType <: Any typ: Any::DataType <: Any
シンボルをどう使うか
Juliaのシンボルは二通りの使われ方をします。ひとつはコードの中での効率的な文字列としてですが、より重要なのがメタプログラミングでの利用です。
先ほど述べたように、Juliaのコードは Expr
というデータ構造で表現できるため、Juliaからこのデータ構造を操作することでプログラムを自由に作ることができます。
さらに、これも先ほど見たようにコードの多くはシンボルで構成されるため、シンボルをうまく操ってプログラムを生成する必要があるわけです。
Rubyなどでもシンボルはあるようなのですが、Rubyでは主に String interning としてシンボルを使っているようです。
Expr
とシンボルを使ってコードを生成してみましょう。
先ほどみた x + 1
というコードを生成してみます。
julia> Expr(:call, :+, :x, 1) :(x + 1)
Expr
を使ってコードができることが分かりました。
コードは実行することができるはずです。これには eval
関数を使います。
julia> eval(Expr(:call, :+, :x, 1)) ERROR: UndefVarError: x not defined
変数 x
が無いというエラーになりました。
では x
の値を定義して、もう一度やってみましょう。
julia> x = 10 10 julia> eval(Expr(:call, :+, :x, 1)) 11
今度は問題なく実行できました。
もっとも、 Expr
を使ってコードを生成するより :(...)
や quote ... end
を使ったほうが楽なので、通常はこちらを使います。
しかし、こういうものを使う場合でも、自分がシンボルを含んだ式を作っているということをはっきりと意識することが重要だと思います。
macro
によるマクロの定義と @<macro>
によるマクロ呼出しで、生成したコードをその場に埋め込むことができます。
julia> macro x_plus_one() :(x + 1) end julia> @x_plus_one 11
マクロはREPLで使うこともありますが、実際のコードでは以下のように関数定義の中で呼び出すことが多いでしょう。
julia> function plus_one(x) @x_plus_one end plus_one (generic function with 1 method) julia> plus_one(3) 4
@x_plus_one
マクロは、 x + 1
というコードを生成しますから、以下のように関数の引数を y
にしてしまうと違った動作をします。
julia> function silly_plus_one(y) @x_plus_one end silly_plus_one (generic function with 1 method) julia> silly_plus_one(3) 11
このとき、silly_plus_one(y)
関数は以下の定義と同じ意味ですから、引数は無視されて外側の変数 x
を拾ってしまうことになります。
:(x + 1)
という Expr
が、 :x
というシンボル名を使っていることを強く意識することが重要です。
function silly_plus_one(y) x + 1 end
もうちょっとシンボルを使った簡単なメタプログラミングの実例を見てみましょう。
次のコードは、AからZまでのアルファベットのASCIIコードを保持する定数 ord_A
, ord_B
, ..., ord_Z
を生成するプログラムです。
julia> for char in 'A':'Z' @eval const $(symbol(string("ord_", char))) = $(Int(char)) end julia> ord_A 65 julia> ord_G 71
AからZまですべての定数を手で書いたら大変ですので、メタプログラミングの技法を使うわけです。
ここでは、定数名 ord_<X>
をまず文字列として作り、そこからシンボルを symbol
関数で生成しています (この部分は symbol("ord_", char)
でも問題ありませんが、説明のため一度文字列として変数を生成してからシンボルに変換しています)。
julia> string("ord_", 'A') "ord_A" julia> symbol(string("ord_", 'A')) :ord_A
$(...)
はスプライシング(splicing)や挿入(interpolation)などと言って、コードを評価してその部分に差し込むことができる仕組みです。
例えば、最初の char = 'A'
の段階では、
@eval const $(symbol(string("ord_", 'A'))) = $(Int('A'))
すなわち、
@eval const $(:ord_A) = $(65)
となって、これが quote ... end
と eval
の機能を合わせた @eval
マクロにより
const ord_A = 65
と変換されて、通常の定数の宣言のように振る舞います。 このように、シンボルを使ってコードの変数を生成することで、Julia自身でJuliaのプログラムを自由に生成することができます。
Juliaのメタプログラミングを理解するためにはシンボルをちゃんと理解することが欠かせません。 メタプログラミングをしなくても十分Juliaは強力な言語ですが、これらを使いこなすことでさらに冗長なコードを排除したりパフォーマンスを上げることも可能です。 メタプログラミングについては、Juliaのマニュアル Metaprogramming に詳細な説明がありますので、このあたりに興味のある方は是非確認してみてください。