変数のスコープ

`

変数のスコープ

変数の スコープ とは、変数から参照できるコードの領域のことです。 変数のスコープを使うと変数名の競合を避けることができます。 この概念は直感的です。 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のグローバルスコープである点に注意してください。

`

ローカルスコープ

ほとんどのコードブロックで新しいローカルスコープが導入されます。(完全なリストは上のを参照) ローカルスコープは親のローカルスコープにあるすべての変数を、読み書きともに引き継ぎます。 さらに、親のグローバルスコープブロック(グローバルなifbeginに囲まれたブロック)に割り当てられたグローバル変数を引き継ぎます。 また、内側のスコープにある変数は親のスコープからなんらかの限定的なアクセスによって取り出すことはできません。

以下の規則と例はローカルスコープに関するものです。 ローカルスコープに新しく導入される変数は、親のスコープに逆伝播しません。 例えば、ここにある$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

スコープブロック内のlocalglobalのキーワードの位置は共に無関係です。 下記のものは、直前の例と同等です(表記としては良くないですが)

julia> for i = 1:10
           z = i
           global z
       end

julia> z
10

localglobalのキーワードは、例えばlocal x, y = 1, 2のように、分割代入にも適用されます。 この場合、キーワードはすべての列挙した変数に影響します。

ローカルスコープは大抵のブロックキーワードで導入されますが、注目すべき例外はbeginifです。

ローカルスコープでは、すべての変数を親のグローバルスコープブロックから、以下の場合を除き引き継ぎます。

このように、グローバル変数が引き継ぐのは、読取りだけで、書込みは引き継ぎません。

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には、組込みの効率的な偶数性や奇数性を確認するisevenisoddといった関数があるので、 上記の例は、効率的な設計ではなくスコープの例としてだけ考えるべきでしょう。

`

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宣言によってこのパフォーマンスの問題が解決します。

ローカル変数は全く異なります。 ローカル変数が一定の時は、自動的に決定可能です。 そのため、定数の宣言は必要なく、実のところ対応していません。

functionstructキーワードで実行されるような、特殊なトップレベルの代入は、デフォルトでは定数です。

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