りんごがでている

何か役に立つことを書きます

Juliaのシンボルとは?

先ほど何となくstackoverflowのJuliaに関する投稿を見ていたら、なるほど確かに最初は分からないかもしれないなと思う疑問と、それに対する分かりやすいKarpinski氏の回答があったので紹介しようと思います。

stackoverflow.com

シンボル(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 ... endeval の機能を合わせた @eval マクロにより

const ord_A = 65

と変換されて、通常の定数の宣言のように振る舞います。 このように、シンボルを使ってコードの変数を生成することで、Julia自身でJuliaのプログラムを自由に生成することができます。

Juliaのメタプログラミングを理解するためにはシンボルをちゃんと理解することが欠かせません。 メタプログラミングをしなくても十分Juliaは強力な言語ですが、これらを使いこなすことでさらに冗長なコードを排除したりパフォーマンスを上げることも可能です。 メタプログラミングについては、Juliaのマニュアル Metaprogramming に詳細な説明がありますので、このあたりに興味のある方は是非確認してみてください。