変数のスコープ
変数の スコープ とは、変数から参照できるコードの領域のことです。 変数のスコープを使うと変数名の競合を避けることができます。 この概念は直感的です。 2つの関数が引数に同じ名前のx
を使っていても、2つのx
が同一のものを参照することなく利用できる、というものです。 同様に、コード内の別のブロックで、同じ名前を使っていても、それぞれが同一のものを参照することなく利用できる場合が数多くあります。 同じ名前の変数がいつ同じものを参照し、いつ参照しないのかという規則を、スコープ規則といいます。 このセクションで詳しく説明します。
言語の構文の中には スコープブロック 、つまり変数に対するスコープとして適切なコード領域が決まっているものがあります。 変数のスコープはソースの任意の行を割当てることはできません。以下のいずれかを割り当てます。 Juliaには主に2種類のスコープがあります。 グローバルスコープ と ローカルスコープ です。 後者はネストすることができます。 各構文の導入しているスコープは、
- 他のグローバルスコープ内でのみネストが可能なスコープブロック
- グローバルスコープ
+ モジュール、ベアモジュール
+ 対話プロンプト(REPL)
- ローカルスコープ (ネスト禁止)
+ (可変な) struct, マクロ
- どこでもネストが可能なスコープブロック(グローバルでもローカルでも)
- ローカルスコープ
+ for, while, try-catch-finally, let
+ 関数 (構文、無名関数 、ブロック)
+ 内包表記, ブロードキャスト-融合
この表に記載のない注目すべきものは、 begin ブロックとif ブロックです。 これらは、新たなスコープブロックを 導入しません。 どちらのスコープも後述のような少し違った規則に従います。
Juliaはレキシカルスコープを 使用しています。これは、呼び出し側のスコープを引き継がず、定義されたスコープを引き継ぐという意味です。 例えば、以下のコードでは、foo
の中のx
は、モジュールBar
のグローバルスコープにあるx
を参照しています。
julia> module Bar
x = 1
foo() = x
end;
そして、foo
が使われる場所のスコープにある x
は参照しません。
julia> import .Bar
julia> x = -1;
julia> Bar.foo()
1
このように、レキシカルスコープ は変数のスコープは、ソースコードのみから推論できることを意味します。
グローバルスコープ
各モジュールは新しいグローバルスコープを導入するので、他のすべてのモジュールと分離しています。 すべてを包括するグローバルスコープは存在しません。 モジュールには他のモジュールの変数を自身のスコープに導入することができます。 これはusing または import文を通じて、あるいはドット表記を使った限定的なアクセスを通じて導入できます。 つまり各モジュールはいわゆる 名前空間 です。 変数の束縛を変更できるのは、グローバルスコープ内のみで、モジュール外では、できない点に注意してください。
julia> module A
a = 1 # a global in A's scope
end;
julia> module B
module C
c = 2
end
b = C.c # can access the namespace of a nested global scope
# through a qualified access
import ..A # makes module A available
d = A.a
end;
julia> module D
b = a # errors as D's global scope is separate from A's
end;
ERROR: UndefVarError: a not defined
julia> module E
import ..A # make module A available
A.a = 2 # throws below error
end;
ERROR: cannot assign variables in other modules
対話プロンプト(別名 REPL)はモジュールMain
のグローバルスコープである点に注意してください。
ローカルスコープ
ほとんどのコードブロックで新しいローカルスコープが導入されます。(完全なリストは上の表を参照) ローカルスコープは親のローカルスコープにあるすべての変数を、読み書きともに引き継ぎます。 さらに、親のグローバルスコープブロック(グローバルなif
やbegin
に囲まれたブロック)に割り当てられたグローバル変数を引き継ぎます。 また、内側のスコープにある変数は親のスコープからなんらかの限定的なアクセスによって取り出すことはできません。
以下の規則と例はローカルスコープに関するものです。 ローカルスコープに新しく導入される変数は、親のスコープに逆伝播しません。 例えば、ここにある$z$はトップレベルのスコープに導入されません。
julia> for i = 1:10
z = i
end
julia> z
ERROR: UndefVarError: z not defined
(これ以降の例では、トップレベルが、新規に起動されたREPLなどの潔白な作業領域をもつ、グローバルスコープであることを想定している点に、 注意してください。)
ローカルスコープ内で、local
キーワードを使って、変数を強制的に新しいローカル変数にすることができます。
julia> x = 0;
julia> for i = 1:10
local x # this is also the default
x = i + 1
end
julia> x
0
ローカルスコープ内でglobal
変数を使ってグローバル変数に代入することができます。
julia> for i = 1:10
global z
z = i
end
julia> z
10
スコープブロック内のlocal
とglobal
のキーワードの位置は共に無関係です。 下記のものは、直前の例と同等です(表記としては良くないですが)
julia> for i = 1:10
z = i
global z
end
julia> z
10
local
とglobal
のキーワードは、例えばlocal x, y = 1, 2
のように、分割代入にも適用されます。 この場合、キーワードはすべての列挙した変数に影響します。
ローカルスコープは大抵のブロックキーワードで導入されますが、注目すべき例外はbegin
とif
です。
ローカルスコープでは、すべての変数を親のグローバルスコープブロックから、以下の場合を除き引き継ぎます。
- 代入によって グローバル 変数が変更されている
- 変数にキーワード
local
をわざわざつけている。
このように、グローバル変数が引き継ぐのは、読取りだけで、書込みは引き継ぎません。
julia> x, y = 1, 2;
julia> function foo()
x = 2 # assignment introduces a new local
return x + y # y refers to the global
end;
julia> foo()
4
julia> x
1
グローバル変数に代入するには、わざわざglobal
を付ける必要があります。
julia> x = 1;
julia> function foobar()
global x = 2
end;
julia> foobar();
julia> x
2
ネストした関数 は親のスコープの local 変数を変更できる点に注意してください。
julia> x, y = 1, 2;
julia> function baz()
x = 2 # introduces a new local
function bar()
x = 10 # modifies the parent's x
ret
urn x + y # y is global
end
return bar() + x # 12 + 10 (x is modified in call of bar())
end;
julia> baz()
22
julia> x, y # verify that global x and y are unchanged
(1, 2)
ネストした関数で親のスコープの ローカル変数を変更できる 理由は、 プライベートな状態を保持する クロージャ
を構成できるようにするためです。 以下の例の $state$ 変数が具体例です。
julia> let state = 0
global counter() = (state += 1)
end;
julia> counter()
1
julia> counter()
2
クロージャの例としては、次の2セクションも参照してください。 最初の例のx
や二番目の例のstate
などは、周囲のスコープから内部の関数に引き継がれており、 捕捉された 変数と呼ばれます。 捕捉された変数がパフォーマンスの困難となりうる問題についての議論がパフォーマンスティップス にあります。
グローバルスコープの引き継ぎとローカルスコープのネストの違いから、 ローカルスコープとグローバルスコープで定義された、変数の代入をおこなう関数が少しちがってきます。 最後の例のbar
をグローバルスコープに移して少し変えることを考えてみましょう。
julia> x, y = 1, 2;
julia> function bar()
x = 10 # local, no longer a closure variable
return x + y
end;
julia> function quz()
x = 2 # local
return bar() + x # 12 + 2 (x is not modified)
end;
julia> quz()
14
julia> x, y # verify that global x and y are unchanged
(1, 2)
上記のネストの規則は、型やマクロの定義には関係ありません。 これは、グローバルスコープのみに表れるからです。 関数の引数のデフォルトとキーワードの評価に関連する特殊なスコープ規則については関数のセクション に記述があります。
関数、型、マクロの定義の内部で使われる変数を導入して行う代入は、必ずしも内部の定義での前に行う必要はありません。
julia> f = y -> y + a;
julia> f(3)
ERROR: UndefVarError: a not defined
Stacktrace:
[...]
julia> a = 1
1
julia> f(3)
4
この挙動は、通常の変数としては少し変に思えるかもしれませんが、名前付き関数では、可能なことです。 名前付き関数は、関数オブジェクトを保持する単なる通常の変数で、定義を行う前に利用します。 これによって、実際に関数が呼ばれる前に定義されている限り、関数の定義をどんな順番でも、直感的で便利に行うことができ、 ボトムアップの順序や事前の宣言にこだわる必要はありません。 ここであげるのは、非効率で相互再帰な方法でおこなう、正の整数が偶数か奇数かを検査する例です。
julia> even(n) = (n == 0) ? true : odd(n - 1);
julia> odd(n) = (n == 0) ? false : even(n - 1);
julia> even(3)
false
julia> odd(3)
true
Juliaには、組込みの効率的な偶数性や奇数性を確認するiseven
やisodd
といった関数があるので、 上記の例は、効率的な設計ではなくスコープの例としてだけ考えるべきでしょう。
Let ブロック
ローカル変数の代入とは違い、let
文は毎回実行時に新しく変数の束縛しメモリを割り当てます。 代入は既存の場所の値を変更し、let
では新しい場所に生成します。 この違いは、通常それほど重要ではなく、検出できるのもスコープ外のクロージャに変数があるときのみです。 let
構文はコンマで区切った一連の代入と変数名を受け取ります。
julia> x, y, z = -1, -1, -1;
julia> let x = 1, z
println("x: $x, y: $y") # x is local variable, y the global
println("z: $z") # errors as z has not been assigned yet but is local
end
x: 1, y: -1
ERROR: UndefVarError: z not defined
代入は順番に評価されます。 それぞれ右辺は左辺の新しい変数が導入される前に評価されます。 そのため、let x = x
のような式も意味があり、2つの変数x
は異なり、別々に格納されています。 こういうlet
の挙動が必要な例を挙げます。
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
global i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
ここでは、変数i
を返すクロージャを生成し。格納します。 しかし、i
は常に同じ変数で、2つのクロージャは全く同等の挙動をします。 let
を新しい束縛のi
を生成するために利用することができます。
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
global i += 1
end
julia> Fs[1]()
1
julia> Fs[2]()
2
begin
構文は新しいスコープを導入しないので、引数のないlet
を使って単に新しいスコープブロックを導入するだけで、 新しい束縛を生成しないのも、役に立つこともあります。
julia> let
local x = 1
let
local x = 2
end
x
end
1
let
は新しいスコープブロックを導入するので、内側のローカル変数 x
は外側のローカル変数 x
と異なります。
For ループと内包表記
for
ループ、while
ループ、内包表記は以下のような挙動を取ります。 ループ本体のスコープの導入されるすべての新しい変数はループの反復ごとに新しくメモリに割当てられて、 ループの本体がlet
ブロックに囲まれているかのようにふるまう。
julia> Fs = Vector{Any}(undef, 2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
for
ループや内包表記の反復で、変数は常に新しい変数です。
julia> function f()
i = 0
for i = 1:3
end
return i
end;
julia> f()
0
しかし、既存の変数を反復の変数として再利用するのも、役に立つときがあります。 これを行うには、outer
キーワードを使うと便利です。
julia> function f()
i = 0
for outer i = 1:3
end
return i
end;
julia> f()
3
定数
変数のよくある使い方として、特定の変化しない値に名前をつけることがあります。 こういった変数はたった一度代入されるだけです。 こうした意図はconst
キーワードを使ってコンパイラに伝えることができます。
julia> const e = 2.71828182845904523536;
julia> const pi = 3.14159265358979323846;
Multiple variables can be declared in a single const
statement:
julia> const a, b = 1, 2
(1, 2)
const
宣言はグローバルスコープにあるグローバル変数に対してだけ行うべきです。 コンパイラがグローバル変数を含むコードを最適化するのは、困難です。 というのもその値(や型でさえも)ほとんどいつでも変わりうるからです。 グローバル変数が変化しない時には、const
宣言によってこのパフォーマンスの問題が解決します。
ローカル変数は全く異なります。 ローカル変数が一定の時は、自動的に決定可能です。 そのため、定数の宣言は必要なく、実のところ対応していません。
function
やstruct
キーワードで実行されるような、特殊なトップレベルの代入は、デフォルトでは定数です。
const
は変数束縛だけに影響する点に注意してください。 変数は(配列のような)可変オブジェクトを束縛してもいいので、変更可能かもしれません。 さらに、定数だと宣言した変数に値を代入しようとする、以下のようなシナリオもありえます。
- 新しい値が定数の型と異なる型の時、エラーが投げられます。
julia> const x = 1.0
1.0
julia> x = 1
ERROR: invalid redefinition of constant x
- 新しい値が定数と同じ型の時、警告が表示される。
julia> const y = 1.0
1.0
julia> y = 2.0
WARNING: redefining constant y
2.0
代入が結果として変数の値を変えない時、メッセージを出さない。
julia> const z = 100
100
julia> z = 100
100
最後の規則は変数の束縛が変わりうる時も、不変オブジェクトに適用されます。
julia> const s1 = "1"
"1"
julia> s2 = "1"
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x00000000132c9638
Ptr{UInt8} @0x0000000013dd3d18
julia> s1 = s2
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x0000000013dd3d18
Ptr{UInt8} @0x0000000013dd3d18
しかし、可変オブジェクトには想定通り警告が表示されます。
julia> const a = [1]
1-element Array{Int64,1}:
1
julia> a = [1]
WARNING: redefining constant a
1-element Array{Int64,1}:
1
定数だと宣言した変数の値を変更することは、もし可能であっても、強く反対します。 例えば、定数を参照するメソッドは、定数の変更される前にすでにコンパイルされて、古い値のまま使われ続けます。
julia> const x = 1
1
julia> f() = x
f (generic function with 1 method)
julia> f()
1
julia> x = 2
WARNING: redefining constant x
2
julia> f()
1