Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1398] ファイルの概要

このコミットは、Go言語のコンパイラと標準ライブラリの一部に対する重要な変更を含んでいます。主な目的は、スライス(Go言語における動的配列)をメソッドのレシーバとして使用できるようにすること、およびhilbertbignumパッケージのコードを元の意図した形に戻すことです。これは、Go言語の初期開発段階における型システムとコンパイラの挙動の成熟を示すものです。

コミット

commit b2dfd787d72772044d1048c8f97b88569e52b87e
Author: Ken Thompson <ken@golang.org>
Date:   Tue Dec 30 14:02:20 2008 -0800

    allow slices (open arrays) to be
    receivers in methods.
    put back original code for hilbert/bignum
    
    R=r
    OCL=21910
    CL=21920

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/b2dfd787d72772044d1048c8f97b88569e52b87e

元コミット内容

このコミットは、以下の変更を加えています。

  • src/cmd/gc/go.h: コンパイラの内部型定義にASLICEを追加。
  • src/cmd/gc/subr.c: コンパイラの型解析ロジックを更新し、スライスを正しくASLICEとして識別・処理するように変更。
  • src/lib/bignum.go: Natural型(スライス型)の多くのメソッドのレシーバをポインタレシーバ(*Natural)から値レシーバ(Natural)に変更。それに伴い、メソッド内部でのポインタのデリファレンス(*x)を削除。
  • test/hilbert.go: bignumパッケージを使用するテストコードから、以前のコンパイラのバグや制限に対する回避策(一時変数の導入や冗長な中間処理)を削除し、より簡潔なメソッドチェーン記述に戻す。

変更の背景

このコミットは、Go言語の非常に初期の段階(2008年12月)に行われたものであり、言語の設計とコンパイラの実装がまだ活発に進化していた時期に当たります。

当時のGo言語では、スライス([]T)をメソッドのレシーバとして使用する際に、コンパイラ(gc)にいくつかの制限やバグが存在していたと考えられます。特に、スライスを値レシーバとして渡した場合の挙動や、メソッドチェーンの最適化に関する問題があったようです。

src/lib/bignum.goNatural型は[]Digitというスライス型として定義されており、その上で多くの数値演算メソッドが実装されています。これらのメソッドがポインタレシーバを使用していたのは、おそらく当時のコンパイラの制限を回避するため、あるいはスライスを値レシーバとして扱う際のセマンティクスがまだ完全に確立されていなかったためと考えられます。

test/hilbert.goに見られる// BUGコメントと、それに対応する冗長なコードは、コンパイラがメソッドチェーンを正しく処理できなかったり、スライスレシーバのメソッド呼び出しで問題が発生したりしたことの証拠です。このコミットは、これらの根本的な問題を解決し、より自然で効率的なGoコードの記述を可能にすることを目的としています。

「put back original code for hilbert/bignum」という記述は、以前にこれらのバグを回避するために導入された一時的なコード変更を元に戻し、コンパイラの改善によって本来意図された簡潔なコードに戻すことを意味しています。

前提知識の解説

1. Go言語のメソッドとレシーバ

Go言語では、関数を特定の型に関連付けることで「メソッド」を定義できます。メソッドの定義には「レシーバ」と呼ばれる特殊な引数が必要です。レシーバには「値レシーバ」と「ポインタレシーバ」の2種類があります。

  • 値レシーバ (Value Receiver): func (t MyType) MyMethod().

    • メソッドが呼び出される際、レシーバの型の値がコピーされてメソッドに渡されます。
    • メソッド内でレシーバの値を変更しても、元の値には影響しません。
    • ただし、スライスやマップ、チャネルなどの参照型の場合、レシーバ自体はコピーされますが、そのレシーバが参照する基底のデータ構造(例: スライスの基底配列)はコピーされません。したがって、メソッド内でスライスの要素を変更すると、元のスライスの基底配列も変更されます。スライスのヘッダ(ポインタ、長さ、容量)がコピーされるため、メソッド内でスライスのヘッダ自体(例: s = append(s, ...)による再割り当て)を変更しても、呼び出し元のスライス変数には影響しません。
  • ポインタレシーバ (Pointer Receiver): func (t *MyType) MyMethod().

    • メソッドが呼び出される際、レシーバの型の値へのポインタが渡されます。
    • メソッド内でポインタを通じてレシーバの値を変更すると、元の値も変更されます。
    • 通常、レシーバの値を変更する必要がある場合や、大きな構造体をコピーするコストを避けたい場合にポインタレシーバが使用されます。

このコミットでは、Natural型がスライスであるため、値レシーバに変更しても、そのスライスの要素(Digit)に対する変更は呼び出し元に反映されます。これは、スライスのセマンティクス(ヘッダは値渡しだが、基底配列は共有される)によるものです。

2. Go言語の配列とスライス

  • 配列 (Array): [N]T

    • 固定長で、要素の型がTであるN個の要素を持つシーケンスです。
    • 配列は値型であり、関数に渡したり代入したりすると、その内容全体がコピーされます。
  • スライス (Slice): []T

    • 可変長で、配列の一部を参照する動的なビューです。
    • スライスは、基底配列へのポインタ、長さ(len)、容量(cap)の3つの要素からなる「スライスヘッダ」で構成されます。
    • スライス自体は値型ですが、そのヘッダが基底配列へのポインタを含むため、複数のスライスが同じ基底配列を共有できます。
    • このコミットの文脈では、Go言語の初期段階で「open arrays」という用語がスライスを指すために使われていたことが示唆されています。

3. Goコンパイラ (gc) の内部構造(初期段階)

Go言語のコンパイラgcは、ソースコードを解析し、中間表現を生成し、最終的に実行可能なバイナリを生成します。このプロセスの中で、型システムは非常に重要な役割を果たします。

  • go.h: コンパイラの内部で使用される型や定数の定義が含まれるヘッダファイルです。enumで定義されるAARRAYAPTRなどは、コンパイラがコードの型を識別するために使用する内部的な分類です。
  • subr.c: コンパイラのサブルーチンやユーティリティ関数が含まれるファイルです。algtype関数は、Goの型をコンパイラ内部の抽象的な型(AARRAY, APTRなど)に分類する役割を担っています。out関数は、コンパイラの様々なフェーズ(例: 型チェック、コード生成)で、特定の型に対する処理を行うための分岐点となることがあります。

このコミットは、これらのコンパイラ内部の定義とロジックを更新することで、スライス型がメソッドレシーバとして正しく扱われるようにしています。

技術的詳細

このコミットの技術的な核心は、Goコンパイラがスライス型をメソッドレシーバとして適切に処理できるようにするための内部的な変更と、それに伴う標準ライブラリのコードの簡素化です。

  1. コンパイラ内部でのスライス型の明確な識別 (src/cmd/gc/go.h, src/cmd/gc/subr.c):

    • 以前は、スライスは内部的に固定長配列と同じAARRAY型として扱われ、その中でt->bound < 0という条件でスライス(動的配列)と区別されていた可能性があります。
    • このコミットでは、go.hASLICEという新しい列挙型が導入されました。これは、コンパイラがスライスをより明確に、そして独立した型として認識するためのものです。
    • subr.calgtype関数は、Goの型をコンパイラ内部の抽象的な型にマッピングする役割を担っています。この関数が更新され、TARRAY型(Goの配列型)のうち、t->bound < 0(Goのコンパイラがスライスを識別するための内部的な慣習)である場合に、新しく定義されたASLICE型を割り当てるようになりました。これにより、コンパイラはスライスを配列とは異なるセマンティクスを持つ型として扱う準備が整いました。
    • また、subr.cout関数内のswitch文にcase ASLICE:が追加されました。これは、コンパイラの型チェックやコード生成のフェーズで、ASLICE型に対して特定の処理を行う必要があることを示しています。これにより、スライスがメソッドレシーバとして使用された際に、コンパイラが正しいコードを生成できるようになります。
  2. bignum.goにおけるレシーバの変更とコードの簡素化:

    • src/lib/bignum.goNatural型はtype Natural []Digitと定義されており、これはスライス型です。
    • このコミットの最も顕著な変更は、Natural型のほとんどのメソッド(IsOdd, Add, Mul, Div, Shl, And, Or, Xor, Cmp, ToStringなど)のレシーバが、ポインタレシーバ(func (x *Natural) ...)から値レシーバ(func (x Natural) ...)に変更されたことです。
    • これに伴い、メソッド内部でレシーバxを使用する際に、以前はポインタのデリファレンス(*x)が必要だった箇所が、直接xを使用するように変更されています。
    • この変更は、コンパイラがスライスを値レシーバとして正しく扱えるようになったことを示しています。スライスは参照型のように振る舞いますが、スライスヘッダ自体は値渡しされます。しかし、そのヘッダが指す基底配列は共有されるため、Natural型のような数値演算ライブラリでは、値レシーバを使用しても基底配列の要素を変更できるため、ポインタレシーバと同等の機能を提供できます。これにより、コードがより簡潔になり、Goのイディオムに沿った記述が可能になります。
  3. hilbert.goにおけるバグ回避策の削除:

    • test/hilbert.gobignumパッケージを利用したテストコードです。このファイルには、以前のコンパイラのバグや制限によって導入されたと思われる、冗長な中間変数や明示的なステップに分割されたメソッド呼び出しが存在していました(例: y1 := x0.Mul(x1); y2 := y1.Mul(x2); ...)。
    • これらの箇所には// BUGというコメントが付されており、当時のコンパイラがメソッドチェーンを正しく最適化できなかったり、スライスレシーバのメソッド呼び出しで問題があったりしたことを示唆しています。
    • このコミットにより、コンパイラの改善がなされたため、これらの回避策が不要となり、コードがa.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));のように、より簡潔なメソッドチェーンの形式に戻されました。これは、コンパイラがスライスレシーバのメソッド呼び出しとメソッドチェーンを正しく処理できるようになったことの直接的な結果です。

これらの変更は、Go言語の型システムが成熟し、コンパイラがより複雑な型(特にスライス)とメソッドの相互作用を効率的かつ正確に処理できるようになる過程を示しています。

コアとなるコードの変更箇所

1. src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -41,7 +41,7 @@ enum
 	ASTRING,
 	APTR,
 	AINTER,
-	AARRAY,
+	ASLICE,
 	ASTRUCT,

 	BADWIDTH	= -1000000000

AARRAYの代わりにASLICEという新しい内部型が導入されました。これは、コンパイラがスライスを配列とは異なる独自の型として認識するためのものです。

2. src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -301,8 +301,8 @@ algtype(Type *t)
 	if(isptr[t->etype])
 		a = APTR;	// pointer
 	else
-	if(t->etype == TARRAY)
-		a = AARRAY;
+	if(t->etype == TARRAY && t->bound < 0)
+		a = ASLICE;
 	else
 	if(t->etype == TSTRUCT)
 		a = ASTRUCT;
@@ -1576,6 +1576,7 @@ out:
 	case ASIMP:
 	case APTR:
 	case ASTRING:
+	case ASLICE:
 		break;
 	}

algtype関数が、TARRAY型でbound < 0(スライスを示す)の場合にASLICEを割り当てるように変更されました。また、out関数内のswitch文にASLICEのケースが追加され、スライス型がコンパイラの処理フローで適切に扱われるようになりました。

3. src/lib/bignum.go

--- a/src/lib/bignum.go
+++ b/src/lib/bignum.go
@@ -130,18 +130,18 @@ export func Nat(x uint) Natural {
 	case 10: return NatTen;
 	}
 	assert(Digit(x) < B);
-	return *&Natural{Digit(x)};	// TODO(rsc): *&
+	return Natural{Digit(x)};
 }


 // Predicates

-func (x *Natural) IsOdd() bool {
+func (x Natural) IsOdd() bool {
 	return len(x) > 0 && x[0]&1 != 0;
 }


-func (x *Natural) IsZero() bool {
+func (x Natural) IsZero() bool {
 	return len(x) == 0;
 }

Natural型(スライス型)のメソッドのレシーバが、ポインタレシーバ(*Natural)から値レシーバ(Natural)に多数変更されました。これにより、コードがより簡潔になり、スライスを値レシーバとして直接扱えるようになりました。

4. test/hilbert.go

--- a/test/hilbert.go
+++ b/test/hilbert.go
@@ -100,12 +100,7 @@ func NewInverseHilbert(n int) *Matrix {
 		x3 := MakeRat(Big.Binomial(uint(n+j), uint(n-i-1)));
 		x4 := MakeRat(Big.Binomial(uint(i+j), uint(i)));
 		x4 = x4.Mul(x4);
-		// BUG a.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));
-		y1 := x0.Mul(x1);
-		y2 := y1.Mul(x2);
-		y3 := y2.Mul(x3);
-		y4 := y3.Mul(x4);
-		a.set(i, j, y4);
+		a.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));
 	}
 }
 return a;

以前のコンパイラのバグに対する回避策として導入されていた冗長な中間変数や明示的なステップが削除され、メソッドチェーンを直接記述できるようになりました。// BUGコメントも削除されています。

コアとなるコードの解説

このコミットのコアとなる変更は、Go言語のコンパイラがスライスをファーストクラスの型として、特にメソッドレシーバの文脈で、より適切に扱えるようにすることです。

  1. コンパイラの型システム強化:

    • src/cmd/gc/go.hsrc/cmd/gc/subr.cの変更は、Goコンパイラの内部型システムを強化するものです。ASLICEという新しい内部型を導入し、algtype関数でスライスを正確にASLICEとして分類することで、コンパイラはスライスを配列とは異なる独自のセマンティクスを持つ型として認識できるようになりました。
    • これにより、コンパイラはスライスをメソッドレシーバとして使用する際に、より正確な型チェックと効率的なコード生成を行うことが可能になります。以前は、スライスが配列の特殊なケースとして扱われていたため、メソッド呼び出しの際に予期せぬ挙動や最適化の問題が発生していた可能性があります。
  2. スライス値レシーバの実現とイディオムの改善:

    • src/lib/bignum.goにおけるNatural型メソッドのレシーバをポインタから値に変更したことは、Go言語のイディオムと表現力を大きく向上させます。Natural型はスライスであるため、その値レシーバはスライスヘッダのコピーを受け取りますが、そのヘッダが指す基底配列は共有されます。これにより、メソッド内でスライスの要素を変更した場合、その変更は呼び出し元にも反映されます。
    • これは、Natural型のような数値型にとって自然な挙動であり、ポインタレシーバを使用するよりもコードが簡潔になります。例えば、x.Add(y)のように直接値を渡して演算結果を得るスタイルは、Goの組み込み型(例: int, string)の操作に近く、より直感的です。
    • この変更は、Go言語がスライスを単なる配列のビューとしてだけでなく、それ自体が豊富なメソッドを持つことができる、より強力なデータ構造として進化していることを示しています。
  3. コンパイラバグの修正とコードの簡素化:

    • test/hilbert.goの変更は、コンパイラのバグが修正されたことの直接的な結果です。以前は、コンパイラがメソッドチェーンやスライスレシーバのメソッド呼び出しを正しく処理できなかったため、開発者は一時変数を使用したり、演算を複数のステップに分割したりする回避策を講じる必要がありました。
    • このコミットにより、これらの回避策が不要となり、コードがより簡潔で読みやすくなりました。これは、コンパイラの成熟と、Go言語の設計思想である「シンプルさと明瞭さ」への回帰を意味します。開発者は、コンパイラの制限を気にすることなく、より自然な形でコードを記述できるようになりました。

総じて、このコミットはGo言語の初期段階において、スライスという重要なデータ構造が、言語の型システムとコンパイラによって完全にサポートされ、その潜在能力を最大限に引き出せるようにするための基盤を築いたものです。これにより、Go言語はより堅牢で表現力豊かな言語へと進化しました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/gcsrc/libディレクトリ)
  • Go言語のメソッドとレシーバに関する一般的な解説(Go公式ブログやGo言語の書籍など)
  • Go言語のスライスに関する一般的な解説(Go公式ブログやGo言語の書籍など)
  • Go言語の初期のコミット履歴と関連する議論(GitHubのコミットログやGoのメーリングリストアーカイブなど)
  • Go言語のコンパイラに関する技術文書(もし公開されているものがあれば)

(注: 2008年当時のGo言語に関する詳細な公式ドキュメントやブログ記事は、現在のものとは異なる可能性があります。この解説は、当時のコードベースとGo言語の一般的な知識に基づいて推測されています。)