Goのスコープについて考えてみよう #golang

January 2, 2017

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

はじめに

Twitterで以下のような投稿をしてみました。

https://twitter.com/tenntenn/status/815807925222412292

この問題は、以下のコード中に存在するスコープの数を聞いている問題です。

package main

import "fmt"

func main() {
	const message = "hello, world"
	fmt.Println(message)
}

まだ回答してない方は、ぜひここでいくつになるか考えてみてください。

さて、ここでヒントを出しましょう。 以下のコードをみてみましょう。

package main

import (
	myfmt "fmt"
)

func main() {
	var string string = "HELLO"
	myfmt.Println(string)
}

このコードに存在する識別子は、どのスコープに存在するでしょう? myfmtはどの範囲で使えるでしょうか? 組込み型のstringは?一方で変数のstringは? それぞれの識別子の有効範囲を考えてみると、自ずと分かってくるかと思います。

それでは答えを考えていきましょう。

なお、この記事を書いた時点でのGoの最新バージョンはGo1.7.4です。

go/typesパッケージを使って識別子が存在するスコープを探す

さて、答えあわせといきたいところですが、せっかくなのでgo/typesパッケージを使って、それぞれの識別子が定義されているスコープを取得してみたいと思います。

まずは、対象となるコードをパースしてASTを取得しましょう。 ここではparser.ParseFile関数を使っています。 詳しいASTの取得の方法は、「ASTを取得する方法を調べる」という記事に書いていますので、そちらを参考にしてください。

fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sample.go", src, 0)
if err != nil {
	log.Fatalln("Error:", err)
}

さて、ファイルを表すASTのノードであるast.Fileが手に入ったので(ここではfという変数)、go/typesパッケージが提供する型チェッカーにかけ、パッケージ情報を取得してみます。

conf := types.Config{Importer: importer.Default()}
pkg, err := conf.Check("main", fset, []*ast.File{f}, nil)
if err != nil {
	log.Fatalln("Error:", err)
}

types.Config構造体は型チェッカーの設定を保持する構造体で、ここではImporterフィールドを指定しています。types.Importerインタフェースは、インポートパスからパッケージ情報を取得する機能を提供するインタフェースです。 なお、パッケージ情報はtypes.Pacakge構造体で表されます。

*types.Config型のCheckメソッドは、型チェックを行うメソッドで以下の3つの機能を提供します。

  • 識別子の解決(identifier resolution)
  • 型推論(type deduction)
  • 定数の評価(constant evaluation)

第1引数は、対象とするパッケージのインポートパスで、今回はmainとしています。 そして第2引数には、パッケージを構成するファイルの情報をASTのノード(*ast.File型)のスライスとして渡します。 最後に、第3引数にはtypes.Info構造体のポインタを渡すのですが、ここでは不要なのでnilにしてあります。 もし、識別子の使用場所や定義場所などの情報を欲しい場合は、UsesフィールドやDefsフィールドのマップを初期化して渡すと、そこに解析結果が入れられます。

Checkメソッドの戻り値は、パッケージ情報であるtype.Pacakge構造体のポインタが取得できます。 今回はここから取得できるスコープの情報を使用して、各識別子のスコープを取得します。

さて、今回対象とするファイル中に登場する識別子をすべて取得するには、ast.Inspect関数を使うのが手っ取り早いでしょう。 なお、ast.Inspect関数についての詳細は、「抽象構文木(AST)をトラバースする」という記事に書いてありますのでぜひ参考にしてください。

ast.Inspect(f, func(n ast.Node) bool {
	if ident, ok := n.(*ast.Ident); ok {
		// TODO: 識別子のスコープを取得する
	}
	return true
})

上記のコードには、一番大切な各識別子が定義されたスコープを取得するコードがありません。 どうすれば、取得できるのでしょうか?

go/typesパッケージでは、スコープは、types.Scope構造体として定義されています。 そして、スコープは木構造体を形成しているので、ChildメソッドやParentで子や親のスコープを取得できます。

*types.Config型のCheckメソッドの戻り値である、*types.Package型からはScopeメソッドを呼ぶことで、パッケージスコープが取得できます。 今回対象とするコードで定義された識別子は、パッケージスコープより下にあることは予想されます。 また、*types.Scope型のInnerMostメソッドを使うことで、指定した位置を内包するスコープのうち最も内側にあるスコープを、メソッドのレシーバであるスコープ以下のスコープ(内側のスコープ)から探します。 そのため、一度識別子が登場した場所を含む最も内側のスコープを探し、そこからその識別子が定義されているスコープを親を辿っていきながら探します。 これは、同じ識別子でも、内側のスコープで定義された識別子が優先されることと一致しています。 *types.Scope型のLookupParentメソッドを使うと、親を辿って対応する識別子のスコープを探すことができます。

上記の処理をコードにすると以下のようになります。

innerMost := pkg.Scope().Innermost(ident.Pos())
s, _ := innerMost.LookupParent(ident.Name, ident.Pos())

innerMostidentが登場した場所のもっとも内側にあるスコープを表します。 そして、LookupParentメソッドで、innerMostから親を辿っていき、identの定義されたスコープを探します。 なお、LookupParentメソッドの第2戻り値には、types.Object型の値が返されますが、ここでは使用しないため_に代入しています。

さて、上記の処理で取得したsがいくつあるかがこの問題を解く鍵です。 ここまで出てきたコードを実行可能なようにすべて載せておきます。

package main

import (
	"fmt"
	"go/ast"
	"go/importer"
	"go/parser"
	"go/token"
	"go/types"
	"log"
)

// sample.go
const src = `package main

import "fmt"

func main() {
	const message = "hello, world"
	fmt.Println(message)
}`

func main() {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "sample.go", src, 0)
	if err != nil {
		log.Fatalln("Error:", err)
	}

	conf := types.Config{Importer: importer.Default()}
	pkg, err := conf.Check("main", fset, []*ast.File{f}, nil)
	if err != nil {
		log.Fatalln("Error:", err)
	}

	scopes := map[*types.Scope]struct{}{}
	ast.Inspect(f, func(n ast.Node) bool {
		if ident, ok := n.(*ast.Ident); ok {
			innerMost := pkg.Scope().Innermost(ident.Pos())
			s, _ := innerMost.LookupParent(ident.Name, ident.Pos())
			if s != nil {
				scopes[s] = struct{}{}
			}
		}
		return true
	})

	fmt.Println("====", len(scopes), "scopes ====")
	for s := range scopes {
		fmt.Println(s)
	}
}

scopesをマップにし、キーを*types.Scope型の値にすることで、重複なくスコープを取得するようにしています。 上記のコードを実行すると以下のように表示されるでしょう。 なおこのコードは、The Go Playground上では、importer.Default関数で取得できるtypes.Importerがうまく動かないので実行できません。

==== 3 scopes ====
package "main" scope 0x4203ff6d0 {
.  func main.main()
}
sample.go scope 0x4203ff7c0 {
.  package fmt
}
function scope 0x42053d630 {
.  const message untyped string
}

このコードで出てくる識別子が定義されている以下のスコープは3つのようです!

  • mainパッケージのスコープ
  • ファイル(sample.go)スコープ
  • main関数のスコープ

出力結果を見れば分かる通り、ファイルスコープでは、importした際に割り当てられるパッケージの識別子が定義されます。ここでは、fmtがそれにあたります。 よく考えてみると、ファイルごとにimport文は書くため、この結果には納得できると思います。

さて、冒頭で挙げた問題の答えば3つが正解でしょうか!? でもよく考えてみると、何かおかしいです。 解析したコードには、組込み関数や組込み型が一切使われていません。 これらを表すpanicintなどの識別子はどのスコープで定義されているのでしょうか?

ユニバーススコープ

実は組込み関数や組込み型、そしてtruefalseなどの定数はユニバーススコープというスコープで定義されています。 このスコープは、どんなGoのコードでも存在し、すべてのスコープのルートとなるスコープです。 ユニバーススコープはtypes.Universeという変数で定義されています。 なお変数として定義する便宜上、すべてのルートとなるスコープですが、Childメソッドからは子となるスコープは取得できません。

では、組込み型を使用する以下のコードを先程の解析プログラムにかけて結果をみてみましょう。

package main

import (
	myfmt "fmt"
)

func main() {
	var string string = "HELLO"
	myfmt.Println(string)
}

結果は以下のようになります。

==== 4 scopes ====
package "main" scope 0x4203ff6d0 {
.  func main.main()
}
sample.go scope 0x4203ff7c0 {
.  package myfmt ("fmt")
}
universe scope 0x4203fe1e0 {
.  builtin append
.  type bool bool
.  type byte byte
.  builtin cap
.  builtin close
.  builtin complex
.  type complex128 complex128
.  type complex64 complex64
.  builtin copy
.  builtin delete
.  type error interface{Error() string}
.  const false untyped bool
.  type float32 float32
.  type float64 float64
.  builtin imag
.  type int int
.  type int16 int16
.  type int32 int32
.  type int64 int64
.  type int8 int8
.  const iota untyped int
.  builtin len
.  builtin make
.  builtin new
.  nil
.  builtin panic
.  builtin print
.  builtin println
.  builtin real
.  builtin recover
.  type rune rune
.  type string string
.  const true untyped bool
.  type uint uint
.  type uint16 uint16
.  type uint32 uint32
.  type uint64 uint64
.  type uint8 uint8
.  type uintptr uintptr
}
function scope 0x42053d630 {
.  var string string
}

ユニバーススコープを含めて4つのスコープが存在することが分かるかと思います。 fmtパッケージに別名をつけたmyfmtがちゃんとファイルスコープで定義されているのも分かりますね。

実は、スコープには以下の4種類があります。

  • ユニバース
  • ファイル
  • パッケージ
  • ローカル

そして、それぞれの種類のスコープでは以下のオブジェクト(types.Objectに対応する)が定義できます。

ユニバース ファイル パッケージ ローカル
組込み関数(types.Bultin)
nil(types.Nil)
定数(types.Const)
型名(types.TypeName)
関数(types.Func)
変数(types.Var)
パッケージ名(types.PkgName)
ラベル(Label)

おわりに

さて、長々と説明してきましたが、答えは4つです! 今回の問題は「go/types: The Go Type Checker」というドキュメント中に出てくるものを参考にしました。 このドキュメントはgo/typesパッケージについて詳しく書いてあり、Goに関する知識も身につくため、ぜひ読んでみてください。 なお、読むためにはある程度、他のgoパッケージについても知っておく必要があるため、go/parserパッケージやgo/astパッケージくらいは触っておいた方が良さそうです。 これらのパッケージについて知りたい方は公式ドキュメントを読むか、私の他のQiitaの記事でgoパッケージについて書いているので、ぜひそちらも読んでみてください。

ちなみに私は最初2つだと思ってました!まだまだですね!