[インデックス 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