変数とスコープと名前解決の話

はじめに

多くの言語にはスコープの概念がある。そして、スコープにより「スコープの外側」「内側」という概念が生まれる。この時、スコープの外側の変数名を内側からどのように参照すべきかが問題となる。この変数の名前解決については、だいぶわかったつもりでいたんだけど、全然わかってなかったので、ここにまとめておく。以下、いくつかの言語の比較をするが、どの言語が良いとか悪いとか言うつもりは全くない。ただ「言語ごとに結構ポリシーが違うんだなぁ」ということを共有したくてこの記事を書いた。

経緯

いま、Pythonの講義ノートを書いているのだが、そのスコープのところでこんなことを書いた。

これは、ローカル変数によるグローバル変数の上書きの例として出したもので、こんなコードだ。

a = 10
def func():
    print(a)
    a = 20

すると、こういう指摘をいただいた。

この指摘は正しく、先程のコードはエラーになる。つまり、

a = 10
def func():
    print(a)

このコードはグローバル変数aを表示する。

a = 10
def func():
    a = 20

このコードはローカル変数aを宣言する。

そして、

a = 10
def func():
    print(a)
    a = 20

このコードは、関数func内にaへの代入文があるのでaはローカル変数とみなされ、print文実行時には未定義なのでエラーとなる。

どうしてこうなるかは、西尾さんの指摘

や、スクラップボックスの記事の通り。

で、このあたりの名前解決の話をつらつら書こうかな、と思う。

グローバル変数とローカル変数

先のPythonのコード、

a = 10
def func():
    print(a)
    a = 20

は、print文ではグローバル変数を参照し、代入文ではローカル変数の宣言となることを意図して書いたものだが、前述の通りエラーとなる。C++で同じことをするとこんな感じになるだろう。

#include <iostream>

int a = 10;

void func(){
  std::cout << a << std::endl; //グローバル変数が参照される
  int a = 20; //ローカル変数が宣言される
}

int main(){
  func();
  std::cout << a << std::endl; // グローバル変数は影響を受けない
}

これは意図どおり、最初の表示はグローバル変数を参照し、次の代入文ではaはローカル変数として扱われる。C++は「変数の宣言」と「代入」が別れている言語だ。したがって、int aとある時点で変数宣言であるとわかる。

しかしPythonは変数宣言を代入によって行う。したがって、

a = 20

とある時に、これが変数宣言となるか、別の場所で宣言された変数への代入になるかは文脈によって決まる。

a = 10
def func():
    a = 20

などとすると、Pythonではa = 20はローカル変数の宣言とみなされ、グローバル変数は影響を受けない。

C++はローカルスコープ内からグローバル変数を触ることができる。

#include <iostream>

int a = 10;

void func(){
  a = 20; // グローバル変数の書き換え
}

int main(){
  func();
  std::cout << a << std::endl; // func内で書き換えられた値が表示される
}

Pythonで同様なことをするには、global宣言により、変数aがグローバル変数であることを明示する必要がある。

a = 10
def func():
    global a
    a = 20 # グローバル変数`a`の値が書き換えられる。

さて、RubyもPythonのように代入により変数の宣言を行う言語だが、グローバル変数は$をつけるという文法だ。したがって、Pythonのように「ローカル変数なのか、グローバル変数なのか」という問題はおきない。

$a = 10
def func
    $a = 20
end

まとめるとこんな感じ。グローバルとローカルに同じ名前の変数がある場合、

  • Rubyはグローバル変数に$をつけるので、ローカルとグローバルの変数名がぶつからない。
  • Pythonは、ローカルスコープからグローバル変数を参照できる。しかし、代入文があると、そのスコープ全体にわたってローカル変数であるとみなされる。
  • C++は宣言と代入が別れているので、ローカル変数宣言があるまではグローバル変数と、宣言があった後はローカル変数とみなされる。

ネストするスコープ

Pythonの場合

Pythonは、関数の中の関数、「関数内関数」を作ることができる。この時、関数のスコープがネストするので、先程「グローバル変数」「ローカル変数」で起きたことと同じことが「外側の関数の変数」「内側の関数の変数」でおきる。

def func1():
    a = 10
    def func2():
        a = 20
    print(a)

func1() #=> 10

外側の関数でローカル変数aが定義されており、それは内側の関数func2から参照できる。また、func2内で変数を書き換えても、外側に影響を与えない。

def func1():
    a = 10
    def func2():
        a = 20
    func2()
    print(a)

func1() #=> 10

しかし、global指定によりローカルスコープからグローバル変数を修正できたように、nonlocal指定をすれば、外側のローカル変数を修正できる。

def func1():
    a = 10
    def func2():
        nonlocal a
        a = 20
    func2()
    print(a)

func1() #=> 20

これを利用して、nonlocalはクロージャの状態を覚えさせるのに使われたりする。

Ruby

Rubyはどうだろう?Rubyもメソッド内メソッドを作ることはできるが、スコープはネストしない。例えばこんなコードを書いてみる。

def func1
  a = 10
  def func2
    puts a
  end
  func2
end

func1

Python同様に、内側のメソッドfunc2から、外側のメソッドfunc1のローカル変数を参照することを意図したコードだが、これはエラーとなる。Rubyのメソッド内メソッドはスコープをネストさせず、上記のコードは以下と等価だ。

def func1
  a = 10
  func2
end

def func2
  puts a
end

func1

func1func2は同じスコープに所属し、それぞれ独立したスコープを持つ(親子関係がない)。したがって、func2からfunc1のローカル変数aを参照しようとしても「知らないよ」となる。

Rubyにおいてメソッド内メソッドとは、「外側のメソッドが実行された時に内側のメソッドが定義される」という動作をするものだ。したがって、メソッド内メソッドを、メソッドの外側から呼ぶことができる。

def func1
  def func2
    puts "Hello func2"
  end
end

func1
func2

これを見ても、func1func2が同じスコープに所属していることがわかると思う。Pythonで同じことをするとエラーになる。

def func1():
    def func2():
        print("Hello func2")

func1()
func2() #=> NameError: name 'func2' is not defined

C++

C++ではどうだろうか?C++においては、関数オブジェクトを使えば関数内関数と似たようなことが実現できる。

#include <iostream>
  
void func1(){
  struct {
    void operator()(){
      std::cout << "Hello func2" << std::endl;
    }
  }func2;
  func2();
}

int main(){
  func1(); // => "Hello func2"
}

これはfunc1内に定義された関数オブジェクトfunc2を実行している。もちろんfunc2func1のスコープ内にあり、外からは見ることができない。

さて、内側の関数func2から外側のローカル変数を触れるだろうか?やってみよう。

#include <iostream>

void func1(){
  int a = 10;
  struct {
    void operator()(){
      a = 20;
    }
  }func2;
  func2();
  std::cout << a << std::endl;
}

int main(){
  func1();
}

これはfunc1のローカル変数afunc2から触りに行こうとしたものだが、コンパイル時にこんなことを言われて怒られる。

$ g++ nest2.cpp
nest2.cpp: In member function 'void func1()::<unnamed struct>::operator()()':
nest2.cpp:7:7: error: use of local variable with automatic storage from containing function
    7 |       a = 20;
      |       ^
nest2.cpp:4:7: note: 'int a' declared here
    4 |   int a = 10;
      |       ^

エラーメッセージをよく読むと「内側の関数からautomatic storageのローカル変数を触ろうとしているよ」と言われているので、外側の変数にstaticをつけてみよう。

#include <iostream>
  
void func1() {
  static int a = 10; // staticをつけた
  struct {
    void operator()() {
      a = 20;
    }
  } func2;
  func2();
  std::cout << a << std::endl;
}

int main() {
  func1();
}
$ g++ nest3.cpp
$ ./a.out
20

問題なく実行できた。staticをつけなかった場合に怒られたのは、内側から外側の変数を触りにいこうとした時に、その変数のアドレスが決まらないためだ。staticをつければ変数のアドレスが決まるので内側から表示することも修正することもできる。

Java

Javaはどうだろうか。とりあえず関数内にクラスを作り、関数内で宣言された変数を参照してみよう。

class nest1 {
  
  void func1(){
    int a = 10;
    class inner{
      void func2(){
        System.out.println(a); //外側のローカル変数を「参照」する
      }
    }
    (new inner()).func2();
  }

  public static void main(String[] args){
    (new nest1()).func1();
  }
}

func1内に定義されたローカル変数aを、func1内に定義されたinnerクラスのメソッドfunc2から参照している。このコードは問題なく実行できる。

$ javac nest1.java
$ java nest1
10

次に、内側からローカル変数の値を修正してみよう。

class nest2 {

  void func1(){
    int a = 10;
    class inner{
      void func2(){
        a = 20;  //外側のローカル変数を「修正」する
      }
    }
    (new inner()).func2();
  }

  public static void main(String[] args){
    (new nest2()).func1();
  }
}

これはコンパイル時に怒られる。

$ javac nest2.java
nest2.java:7: エラー: 内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要があります
        a = 20;
        ^
エラー1個

実は、内部クラスから外側のローカル変数を触る場合、その変数はfinal、もしくはエラーメッセージにあるように、「事実上のfinal (effectively final)」である必要がある。Javaは、内部クラスから外側のローカル変数を触りにいく時、もしその変数がfinal宣言されていなくても、それをfinalとみなす。したがって、「外側のローカル変数は参照はできるが、変更は許さない」というポリシーだ。

ネストするスコープのまとめ

まとめるとこんな感じ。

  • Pythonの関数内関数はスコープをネストさせ、グローバル変数とローカル変数の場合と同じような名前解決をする。
  • C++はスコープをネストさせ、外側のローカル変数がstaticなら内側から参照、値の代入ができる。
  • Javaはスコープをネストさせ、内側から外側のローカル変数の参照は許すが代入は許さない(final指定を要求する)
  • Rubyはスコープをネストさせない

宣言時に存在しない変数の扱い

名前解決といえば、関数宣言時に存在しない名前をどうするか、という問題がある。例えばPythonでこんな関数を定義する。

def func():
    print(a)

この関数を定義した時には変数aは宣言されていない。しかし、この関数定義はエラーにならない。実行前にaが宣言されるかもしれないからだ。

def func():
    print(a)

a = 10 # ここでaを宣言する
func() #=> 10

もちろん、実行時までに宣言されていなければエラーになる。

def func():
    print(a)

func() #=> NameError: name 'a' is not defined

つまり、Pythonでは関数宣言時は、グローバル変数と思しき変数については名前解決を棚上げしなくてはならない。

ちなみに、Rubyでは宣言されていないグローバル変数表示しようとしてもエラーにはならず、変数はnilとなる。

def func
    puts $a
end

func #=> nil

さて、C++においては、関数宣言時に必要な名前が全て宣言されていなければならない。つまり、以下のようなコードはエラーになる。

void func(){
  a = 10;
}

int a;

もちろんクラスの存在しないメンバへのアクセスもコンパイルエラーになる。

class hoge {
  void func() {
    this->a = 10; // error: 'class hoge' has no member named 'a'
  }
};

しかし、これをテンプレートにするとコンパイルできる。

template <class T>
class hoge { 
  void func() { 
    this->a = 10; // ここでエラーがおきない
  } 
};

これは、テンプレートの「二段階名前解決(Two phase name lookup)」による。テンプレートにおいて、thisなどで修飾された変数の名前解決は、実体化まで棚上げされる。なぜなら、「テンプレートの特殊化」によって、実体化時にはその変数が宣言されているかもしれないからだ。

それを利用して、メンバ関数宣言時には宣言されていなかったメンバ変数を、テンプレートの特殊化で後から追加できる。

わざとらしいコードだが、例えばこんな感じになるだろう。

template <class T>
struct hoge {
};

template <class T>
struct subhoge : public hoge<T> {
  void func() {
    std::cout << this->a << std::endl; // この時点ではaは宣言されていない
  }
};

template <>
struct hoge<int> {
  int a; //特殊化により親クラスにaを追加する
  hoge()
      : a(10) {
  }
};

int main() {
  subhoge<int> sh;
  sh.func(); // => 10
}

テンプレートのクラスhogeがあり、それをsubhogeが継承している。subhoge::func内でメンバ変数this->aを参照しているが、この時点では親クラスにも自分にもそんなメンバ変数は存在しない。

しかし、テンプレートの特殊化により、親クラスが<int>である時メンバ変数aが宣言された。それにより、subhoge<int>で実体化される時にはaがあるので問題なく参照できる。

まとめ

名前解決は、言語処理系の設計の根幹に関わるためか、言語によって扱いがかなり異なる。そこから「言語設計」のポリシーが透けて見えて面白い。個人的にはRubyの「メソッド内メソッドがネストしたスコープを作らない」というのに驚いた。しかし難しいね・・・