[インデックス 15858] ファイルの概要
このコミットは、Go言語のパーサーパッケージ(go/parser
)における変更です。具体的には、メソッド式(method expression)をメソッド値(method value)に置き換えることで、コードの簡潔性とGo言語のイディオムへの適合性を向上させています。
コミット
commit 5ae4012b64024c3861f3c9e0f6139a145e31a80f
Author: Robert Griesemer <gri@golang.org>
Date: Wed Mar 20 15:03:30 2013 -0700
go/parser: use method values
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7858045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5ae4012b64024c3861f3c9e0f6139a145e31a80f
元コミット内容
src/pkg/go/parser/parser.go
ファイルにおいて、parseSpecFunction
型の定義が変更され、それに伴い、この型の関数を呼び出す箇所や、parser
型のメソッドをこの型に代入する箇所が修正されています。具体的には、*parser
型のレシーバーを明示的に渡す形式から、メソッド値を利用する形式へと変更されています。
変更の背景
Go言語には「メソッド式 (method expression)」と「メソッド値 (method value)」という概念があります。
- メソッド式 (
T.Method
または(*T).Method
): これは、レシーバーを最初の引数として明示的に受け取る関数を生成します。例えば、(*parser).parseValueSpec
はfunc(p *parser, ...)
のようなシグネチャを持つ関数として扱われます。 - メソッド値 (
instance.Method
): これは、特定のインスタンスにメソッドをバインドした関数を生成します。この関数は、レシーバーを引数として明示的に受け取る必要がなく、あたかも通常の関数のように呼び出すことができます。例えば、p.parseValueSpec
はfunc(...)
のようなシグネチャを持つ関数として扱われ、p
(レシーバー) はその関数が生成された時点でバインドされています。
このコミットの背景には、コードの可読性とGo言語のイディオムへの準拠を向上させる目的があります。メソッド値を使用することで、関数呼び出しの際にレシーバーを繰り返し渡す必要がなくなり、コードがより簡潔になります。特に、パーサーのような再帰的に処理を行うコンポーネントでは、メソッド値の利用はコードの記述をより自然にします。
前提知識の解説
Go言語のパーサー (go/parser
, go/ast
, go/token
)
Go言語のソースコードを解析するために、標準ライブラリにはgo/token
、go/parser
、go/ast
という3つの主要なパッケージがあります。
go/token
: Go言語の字句トークン(キーワード、識別子、演算子など)を定義します。また、ソースコード内の位置情報(行、列、バイトオフセット)を追跡するためのFileSet
やPos
といった型も提供します。go/parser
: Goのソースファイルを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築する役割を担います。ParseFile
関数が主要なエントリポイントで、ソースコードを読み込み、go/token
で定義されたトークンを使ってASTを生成します。go/ast
: 抽象構文木(AST)のノードを表すデータ構造を定義します。ast.File
がGoソースファイルのASTのルートノードとなり、ast.GenDecl
、ast.Spec
、ast.Stmt
などがASTの各要素を表します。これらのノードは、コードの構造と意味を階層的に表現します。
Go言語のメソッド値とメソッド式
前述の通り、Go言語ではメソッドを関数のように扱うことができます。
- メソッド式:
Type.Method
の形式で、レシーバーを最初の引数として取る関数を返します。例えば、(*parser).parseValueSpec
はfunc(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
のような関数型になります。 - メソッド値:
instance.Method
の形式で、特定のインスタンスにバインドされた関数を返します。この関数はレシーバーを引数として取りません。例えば、p.parseValueSpec
はfunc(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
のような関数型になります。
このコミットは、コード内でメソッド式を使用していた箇所をメソッド値に切り替えることで、より自然な関数呼び出しの構文を実現しています。
ast.GenDecl
, ast.Spec
, ast.Stmt
これらはgo/ast
パッケージで定義されるASTの主要なノードタイプです。
ast.GenDecl
(Generic Declaration):import
,const
,type
,var
といった一般的な宣言を表します。例えば、import ("fmt"; "net/http")
のようなブロック全体がGenDecl
になります。ast.Spec
(Specification):GenDecl
の内部にある個々の宣言の仕様を表すインターフェースです。例えば、import "fmt"
の"fmt"
の部分や、const x = 1
のx = 1
の部分がSpec
に該当します。具体的な型としてはast.ImportSpec
,ast.ValueSpec
,ast.TypeSpec
などがあります。ast.Stmt
(Statement): Goプログラムの実行可能な単位である文(ステートメント)を表すインターフェースです。代入文、if文、for文、関数呼び出しなど、多岐にわたる文がこれに該当します。
iota
キーワード
iota
はGo言語のconst
宣言内で使用される特殊な識別子で、連続する定数を定義する際に自動的にインクリメントされるカウンタとして機能します。const
ブロックの開始時に0
にリセットされ、各定数定義で1
ずつ増加します。このコミットではiota
が直接変更されているわけではありませんが、parseSpecFunction
の引数としてiota
が渡されていることから、定数宣言の解析に関連する部分であることがわかります。
技術的詳細
このコミットの技術的な核心は、go/parser
パッケージ内のparseSpecFunction
という型エイリアスの変更と、それに伴う関連コードの修正です。
元々、parseSpecFunction
は以下のように定義されていました。
type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
この定義では、p *parser
というレシーバーを明示的に引数として受け取る関数型でした。これは、(*parser).parseValueSpec
のようなメソッド式をこの型に代入することを意図していました。メソッド式は、そのメソッドが属する型(この場合は*parser
)のインスタンスを最初の引数として受け取る通常の関数として振る舞います。
しかし、このコミットではparseSpecFunction
の定義が以下のように変更されました。
type parseSpecFunction func(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
p *parser
という引数が削除されています。これにより、この型はレシーバーを明示的に受け取らない関数型となりました。この変更の目的は、p.parseValueSpec
のような「メソッド値」をこの型に代入できるようにすることです。メソッド値は、特定のインスタンス(この場合はp
)にバインドされた関数であり、呼び出し時にレシーバーを明示的に渡す必要がありません。
この型定義の変更に伴い、parser.go
内の以下の箇所が修正されました。
-
parseGenDecl
関数内でのf
の呼び出し: 変更前:list = append(list, f(p, p.leadComment, keyword, iota))
変更後:list = append(list, f(p.leadComment, keyword, iota))
f
がメソッド値として扱われるようになったため、p
を明示的に渡す必要がなくなりました。 -
parseDecl
関数内でのf
への代入: 変更前:f = (*parser).parseValueSpec
変更後:f = p.parseValueSpec
(*parser).parseValueSpec
はメソッド式であり、p
を引数として受け取る関数型でした。p.parseValueSpec
はメソッド値であり、p
が既にバインドされているため、p
を引数として受け取らない関数型になります。これにより、parseSpecFunction
の新しい定義に合致するようになりました。同様にparseTypeSpec
も変更されています。 -
parseFile
関数内でのp.parseGenDecl
の呼び出し: 変更前:decls = append(decls, p.parseGenDecl(token.IMPORT, (*parser).parseImportSpec))
変更後:decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
ここでも(*parser).parseImportSpec
というメソッド式がp.parseImportSpec
というメソッド値に置き換えられています。
これらの変更により、コードはよりGo言語のイディオムに沿った形になり、parser
インスタンスのメソッドをあたかも通常の関数のように扱うことができるようになりました。これは、コードの簡潔性と可読性の向上に寄与します。
コアとなるコードの変更箇所
src/pkg/go/parser/parser.go
ファイルの以下の行が変更されています。
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2118,7 +2118,7 @@ func (p *parser) parseStmt() (s ast.Stmt) {
// ----------------------------------------------------------------------------
// Declarations
-type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
+type parseSpecFunction func(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
func isValidImport(lit string) bool {
const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
@@ -2237,12 +2237,12 @@ func (p *parser) parseGenDecl(keyword token.Token, f parseSpecFunction) *ast.Gen
tlparen = p.pos
p.next()
for iota := 0; p.tok != token.RPAREN && p.tok != token.EOF; iota++ {
- list = append(list, f(p, p.leadComment, keyword, iota))
+ list = append(list, f(p.leadComment, keyword, iota))
}
trparen = p.expect(token.RPAREN)
p.expectSemi()
} else {
- list = append(list, f(p, nil, keyword, 0))
+ list = append(list, f(nil, keyword, 0))
}
return &ast.GenDecl{
@@ -2343,10 +2343,10 @@ func (p *parser) parseDecl(sync func(*parser)) ast.Decl {
var f parseSpecFunction
switch p.tok {
case token.CONST, token.VAR:
- f = (*parser).parseValueSpec
+ f = p.parseValueSpec
case token.TYPE:
- f = (*parser).parseTypeSpec
+ f = p.parseTypeSpec
case token.FUNC:
return p.parseFuncDecl()
@@ -2398,7 +2398,7 @@ func (p *parser) parseFile() *ast.File {
if p.mode&PackageClauseOnly == 0 {
// import decls
for p.tok == token.IMPORT {
- decls = append(decls, p.parseGenDecl(token.IMPORT, (*parser).parseImportSpec))
+ decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
}
if p.mode&ImportsOnly == 0 {
コアとなるコードの解説
-
parseSpecFunction
型定義の変更:type parseSpecFunction func(p *parser, doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
からtype parseSpecFunction func(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec
に変更されました。 これは、parseSpecFunction
が*parser
のインスタンスを明示的に引数として受け取る必要がなくなり、メソッド値として直接使用できるようになったことを意味します。 -
parseGenDecl
関数内でのf
の呼び出し箇所の変更:list = append(list, f(p, p.leadComment, keyword, iota))
がlist = append(list, f(p.leadComment, keyword, iota))
に変更されました。f
がメソッド値であるため、既にレシーバーp
がバインドされており、呼び出し時に再度p
を渡す必要がなくなりました。同様に、f(p, nil, keyword, 0)
もf(nil, keyword, 0)
に変更されています。 -
parseDecl
関数内でのf
への代入箇所の変更:f = (*parser).parseValueSpec
がf = p.parseValueSpec
に変更されました。(*parser).parseValueSpec
はparser
型のメソッド式であり、p
を引数として受け取る関数型でした。一方、p.parseValueSpec
はp
にバインドされたメソッド値であり、p
を引数として受け取らない関数型です。これにより、parseSpecFunction
の新しい定義に合致するようになりました。parseTypeSpec
についても同様の変更が適用されています。 -
parseFile
関数内でのp.parseGenDecl
の呼び出し箇所の変更:decls = append(decls, p.parseGenDecl(token.IMPORT, (*parser).parseImportSpec))
がdecls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
に変更されました。 ここでも、(*parser).parseImportSpec
というメソッド式が、p.parseImportSpec
というメソッド値に置き換えられています。これにより、parseGenDecl
に渡される関数が、parseSpecFunction
の新しいシグネチャに適合するようになりました。
これらの変更は、Go言語のパーサーコードをよりイディオム的で簡潔なものにするためのリファクタリングであり、Goのメソッド値の強力な機能を示しています。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
go/parser
パッケージ: https://pkg.go.dev/go/parser - Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージ: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go method values explanation (Web Search Result)
- Go parser package design ast token (Web Search Result)
- Go iota keyword (Web Search Result)
- Go ast.GenDecl ast.Spec ast.Stmt (Web Search Result)
- Go by Example: Methods
- Go: Method Expressions vs Method Values