[インデックス 1633] ファイルの概要
このコミットは、Go言語のテストスイートに含まれるtest/chan/powser1.goおよびtest/chan/powser2.goファイルに対するクリーンアップとリファクタリングを目的としています。これらのファイルは、Goのチャネルとゴルーチンを用いて冪級数(Power Series)の演算を実装したサンプルコードであり、Goの並行処理モデルのテストケースとして機能しています。
powser1.goは、冪級数の加算、乗算、微分、積分などの基本的な演算をチャネルベースで表現しており、このコミットでは特にそのコードの簡潔化とGoのイディオムへの適合が図られています。一方、powser2.goはpowser1.goと類似していますが、意図的に異なるテストケースとして残されており、このコミットではpowser1.goほど大規模なクリーンアップは行われていません。
コミット
commit ee9b5a15a145494d574984855a3afe301246c9b8
Author: Rob Pike <r@golang.org>
Date: Fri Feb 6 15:03:14 2009 -0800
powser cleanup.
- don't need *struct
- don't need item/rat both
- closures make the inner slaves easier
- delete some old BUG comments
powser2 is left mostly alone, for variety.
R=rsc
DELTA=134 (2 added, 20 deleted, 112 changed)
OCL=24579
CL=24581
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ee9b5a15a145494d574984855a3afe301246c9b8
元コミット内容
powser cleanup.
- don't need *struct
- don't need item/rat both
- closures make the inner slaves easier
- delete some old BUG comments
powser2 is left mostly alone, for variety.
R=rsc
DELTA=134 (2 added, 20 deleted, 112 changed)
OCL=24579
CL=24581
変更の背景
このコミットの主な背景は、Go言語の初期段階におけるコードベースの成熟と、よりGoらしい(idiomatic Go)コーディングスタイルの確立にあります。コミットメッセージにある「cleanup」という言葉が示す通り、冗長なコードの削除、型の適切な利用、そしてGoの強力な機能であるクロージャの活用を通じて、コードの可読性と保守性を向上させることが目的でした。
具体的には、以下の点が変更の動機となっています。
- 冗長な型エイリアスの排除:
*rat型へのポインタを指すitemという型エイリアスが存在していましたが、これはrat型を直接使用することで不要となり、コードの簡潔化に繋がります。 - ポインタ型から値型への移行:
rat構造体が比較的小さなデータ構造であるため、ポインタを介してアクセスするのではなく、値として直接扱うことで、コードのシンプル化とパフォーマンスの向上が期待できます。これにより、nilポインタのチェックが不要になるなどの利点もあります。 - クロージャの活用: Goのクロージャ(無名関数)は、特定の処理をその場で定義し、外部スコープの変数にアクセスできる強力な機能です。これにより、特にチャネルを用いた並行処理において、ゴルーチン内で実行される「内部のスレーブ」(
go func()で起動される処理)の記述がより簡潔になります。 - 古いコメントの整理: 開発初期段階で残されていた「BUG」コメントは、問題が解決されたか、あるいは設計変更により不要になったため、削除されました。これはコードベースの健全性を保つ上で重要な作業です。
- テストの多様性:
powser2.goは意図的にpowser1.goほどクリーンアップされずに残されました。これは、異なる実装パターンやテストシナリオを維持することで、テストスイートの多様性を確保するためと考えられます。
これらの変更は、Go言語がまだ比較的新しい時期に、その設計思想とベストプラクティスをコードベースに反映させようとするRob Pike氏(Go言語の共同開発者の一人)の意図が強く表れています。
前提知識の解説
このコミットの変更内容を深く理解するためには、以下のGo言語および関連する数学的概念の知識が役立ちます。
Go言語の基本
- 型 (Types): Goは静的型付け言語であり、変数や関数の引数、戻り値には型が明示されます。
- 構造体 (Structs): 複数のフィールドをまとめた複合データ型です。
rat型はこの構造体として定義されています。 - ポインタ (Pointers): 変数のメモリアドレスを保持する型です。GoではC/C++のようなポインタ演算は制限されていますが、値の参照や変更に利用されます。
*Tは型Tへのポインタを表します。 - 値渡しと参照渡し: Goでは、関数の引数はデフォルトで値渡しされます。つまり、引数の値がコピーされて関数に渡されます。ポインタを渡すことで、元の変数を参照し、変更することが可能になります(参照渡しに似た動作)。
- メソッド (Methods): 構造体や任意の型に関連付けられた関数です。レシーバ(
func (u rat)やfunc (u *rat)) を持つことで、その型のインスタンスに対して呼び出すことができます。- 値レシーバ (Value Receiver):
func (u rat) methodName(). メソッド内でレシーバuを変更しても、元の値は変更されません。レシーバのコピーが渡されます。 - ポインタレシーバ (Pointer Receiver):
func (u *rat) methodName(). メソッド内でレシーバuを変更すると、元の値も変更されます。レシーバのポインタが渡されます。
- 値レシーバ (Value Receiver):
- チャネル (Channels): Goの並行処理の根幹をなす機能で、ゴルーチン間で値を安全に送受信するための通信メカニズムです。
make(chan Type)で作成され、ch <- valueで送信、value := <-chで受信します。 - ゴルーチン (Goroutines): Goの軽量な並行実行単位です。
goキーワードを使って関数を呼び出すことで、新しいゴルーチンとして実行されます。 - クロージャ (Closures): 自身が定義された環境(スコープ)の変数を参照できる関数です。Goでは無名関数(匿名関数)としてよく利用され、
go func() { ... }()のようにゴルーチン内で利用されることが多いです。
数学的な概念
- 冪級数 (Power Series): 数学において、
a_0 + a_1*x + a_2*x^2 + ...のように、項が定数と変数のべき乗の積で構成される無限級数です。このコードでは、有理数係数を持つ冪級数の演算(加算、乗算、微分、積分など)をシミュレートしています。 - 有理数 (Rational Number): 整数
pとゼロではない整数qを用いてp/qの形で表せる数です。このコードではrat構造体(numとdenフィールドを持つ)で表現されています。 - 最大公約数 (GCD - Greatest Common Divisor): 2つ以上の整数に共通する約数のうち最大のものです。有理数の約分(簡約化)に用いられます。
技術的詳細
このコミットにおける技術的な変更は多岐にわたりますが、その中心はrat型の扱いと、Goの並行処理におけるイディオムの適用です。
item型エイリアスの廃止とrat型の直接利用
変更前は、type item *rat;という型エイリアスが定義され、rat構造体へのポインタをitemとして扱っていました。しかし、このコミットではitem型が完全に削除され、その代わりにrat型が直接使用されるようになりました。
- 変更前:
type item *rat; - 変更後:
item型定義を削除
これにより、dch構造体のdatチャネルの型がchan itemからchan ratに変更され、put、get、getnなどの関数シグネチャもitemの代わりにratを直接扱うように変更されました。
意図:
この変更は、コードの冗長性を排除し、型システムをより直接的に利用することを目的としています。rat型がポインタとしてではなく、値として扱われるようになったことで、itemという間接的なエイリアスは不要になりました。これにより、コードの意図がより明確になり、理解しやすくなります。
ポインタレシーバから値レシーバへの変更
rat型に関連する多くのメソッド(例: pr, eq, add, mulなど)のレシーバが、ポインタレシーバ (*rat) から値レシーバ (rat) に変更されました。
- 変更前:
func (u *rat) pr() - 変更後:
func (u rat) pr()
意図:
rat構造体はnumとdenという2つのint64フィールドしか持たない、比較的小さなデータ構造です。Goでは、小さな構造体を値渡しすることは、ポインタ渡しよりも効率的である場合があります。値渡しはコピーが発生しますが、ポインタのデリファレンス(間接参照)のオーバーヘッドや、ガベージコレクションの負担を軽減できる可能性があります。また、値レシーバを使用することで、メソッド内でレシーバの値を変更しても元の値には影響しないため、意図しない副作用を防ぐことができます。この変更は、rat型が主に不変な値として扱われるべきであるという設計思想を反映していると考えられます。
getn関数の引数簡略化
getn関数は、複数のチャネルから値を取得する汎用的な関数でしたが、引数n intが削除され、関数内でlen(in)を使ってnの値を動的に取得するように変更されました。
- 変更前:
func getn(in []*dch, n int) []item - 変更後:
func getn(in []*dch) []rat(型もitemからratに変更)
意図:
nの値は常にinスライスの長さに等しいため、冗長な引数を削除することで、関数のシグネチャを簡潔にし、呼び出し側での引数指定ミスを防ぐことができます。これは、Goの関数設計における「必要な情報のみを引数として渡す」という原則に則った変更です。
クロージャ(無名関数)の活用
Add, Cmul, Monmul, Xmul, Rep, Mon, Shift, Mul, Diff, Integ, Binom, Recip, Exp, Subst, MonSubstといった冪級数演算を行う関数の内部で、ゴルーチンを起動する際に使用される無名関数(クロージャ)の記述が簡潔化されました。具体的には、クロージャが外部スコープの変数をキャプチャする能力を利用し、引数を明示的に渡す必要がなくなりました。
- 変更前:
go func(U, V, Z PS){ // ... }(U, V, Z); - 変更後:
go func() { // ... }();
意図:
Goのクロージャは、定義された時点の環境(スコープ)にある変数を自動的に「キャプチャ」します。したがって、ゴルーチン内で使用するU, V, Zなどの変数が、クロージャが定義されたスコープ内で利用可能であれば、それらを明示的に引数として渡す必要はありません。この変更により、コードがより簡潔になり、クロージャの自然な利用方法が示されています。
古いBUGコメントの削除
コード中に散見された// BUG: want array initializerのようなコメントが削除されました。
意図: これらのコメントは、Go言語の初期段階において、配列初期化子のような特定の機能がまだ実装されていなかったり、既存の機能で不便があったりしたことを示しています。このコミットでこれらのコメントが削除されたということは、Go言語の進化に伴い、これらの問題が解決されたか、あるいは現在の設計ではもはや問題とならないと判断されたことを意味します。これは、Go言語の成熟度を示す良い例です。
sys.Exit(0)の削除
main関数の最後にあったsys.Exit(0)の呼び出しが削除されました。
- 変更前:
sys.Exit(0); // BUG: force waiting goroutines to exit - 変更後: 削除
意図:
Goのmain関数は、すべての非デーモンゴルーチン(明示的にruntime.Goexit()を呼び出さない限り)が終了すると、自動的にプログラムが終了します。初期のGoでは、ゴルーチンが適切に終了しない場合にプログラムがハングアップする可能性があったため、明示的なsys.Exit(0)が必要だったのかもしれません。しかし、Goランタイムの改善により、このような明示的な終了は不要となり、むしろコードの冗長性を増すものとなりました。この変更も、Goランタイムの成熟と、よりクリーンなコード記述への移行を示しています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、test/chan/powser1.goに集中しています。
-
item型エイリアスの削除:--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -17,21 +17,19 @@ type rat struct { num, den int64; // numerator, denominator } -type item *rat; - func (u *rat) pr(){ if u.den==1 { print(u.num) } else { print(u.num, "/", u.den) } print(" ") } -
ratメソッドのレシーバ変更 (*ratからratへ):--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -17,21 +17,19 @@ type rat struct { num, den int64; // numerator, denominator } -type item *rat; - -func (u *rat) pr(){\ +func (u rat) pr() {\ if u.den==1 { print(u.num) } else { print(u.num, "/", u.den) } print(" ") } -func (u *rat) eq(c item) bool {\ +func (u rat) eq(c rat) bool {\ return u.num == c.num && u.den == c.den }(同様の変更が
i2tor,itor,end,add,mul,neg,sub,inv,Evaln,Printn,eval,Rep,Mon,Shift,Poly,Mul,Diff,Integ,Binom,Recip,Exp,Subst,MonSubst,check,checkaなど、ratを扱う多くの関数に適用されています。) -
チャネルの型変更 (
itemからratへ):--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -39,7 +37,7 @@ type dch struct {\ req chan int; - dat chan item; + dat chan rat; nam int; } @@ -48,7 +46,7 @@ func mkdch() *dch {\ chnameserial++; d := new(dch); d.req = make(chan int); - d.dat = make(chan item); + d.dat = make(chan rat); d.nam = c; return d; } -
getn関数の引数変更と内部実装の簡略化:--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -119,15 +117,15 @@ func get(in *dch) *rat {\ // Get one item from each of n demand channels -func getn(in []*dch, n int) []item {\ - // BUG n:=len(in);\ +func getn(in []*dch) []rat {\ + n := len(in);\ if n != 2 { panic("bad n in getn") }; req := new([2] chan int); - dat := new([2] chan item);\ - out := make([]item, 2);\ + dat := new([2] chan rat);\ + out := make([]rat, 2);\ var i int; - var it item;\ + var it rat;\ for i=0; i<n; i++ { req[i] = in[i].req; dat[i] = nil; -
クロージャの引数削除:
--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -311,17 +305,17 @@ func Split(U PS) *dch2{\ // Add two power series -func Add(U, V PS) PS{\ +func Add(U, V PS) PS {\ Z := mkPS(); - go func(U, V, Z PS){\ - \tvar uv [] *rat;\ + go func() {\ + \tvar uv []rat;\ for { <-Z.req; uv = get2(U,V);(同様の変更が
Cmul,Monmul,Rep,Mon,Shift,Mul,Diff,Integ,Binom,Recip,Exp,Subst,MonSubstなど、多くの関数に適用されています。) -
BUGコメントとsys.Exit(0)の削除:--- a/test/chan/powser1.go +++ b/test/chan/powser1.go @@ -621,7 +615,7 @@ func check(U PS, c *rat, count int, str string) {\ } const N=10 -func checka(U PS, a []*rat, str string) {\ +func checka(U PS, a []rat, str string) {\ for i := 0; i < N; i++ {\ check(U, a[i], 1, str); } @@ -630,30 +624,28 @@ func main() {\ Init(); if len(sys.Args) > 1 { // print - print("Ones: "); Printn(Ones, 10); - print("Twos: "); Printn(Twos, 10); - print("Add: "); Printn(Add(Ones, Twos), 10); - print("Diff: "); Printn(Diff(Ones), 10); - print("Integ: "); Printn(Integ(zero, Ones), 10); - print("CMul: "); Printn(Cmul(neg(one), Ones), 10); - print("Sub: "); Printn(Sub(Ones, Twos), 10); - print("Mul: "); Printn(Mul(Ones, Ones), 10); - print("Exp: "); Printn(Exp(Ones), 15); - print("MonSubst: "); Printn(MonSubst(Ones, neg(one), 2), 10); - print("ATan: "); Printn(Integ(zero, MonSubst(Ones, neg(one), 2)), 10); + print("Ones: "); printn(Ones, 10); + print("Twos: "); printn(Twos, 10); + print("Add: "); printn(Add(Ones, Twos), 10); + print("Diff: "); printn(Diff(Ones), 10); + print("Integ: "); printn(Integ(zero, Ones), 10); + print("CMul: "); printn(Cmul(neg(one), Ones), 10); + print("Sub: "); printn(Sub(Ones, Twos), 10); + print("Mul: "); printn(Mul(Ones, Ones), 10); + print("Exp: "); printn(Exp(Ones), 15); + print("MonSubst: "); printn(MonSubst(Ones, neg(one), 2), 10); + print("ATan: "); printn(Integ(zero, MonSubst(Ones, neg(one), 2)), 10); } else { // test check(Ones, one, 5, "Ones"); check(Add(Ones, Ones), itor(2), 0, "Add Ones Ones"); // 1 1 1 1 1 check(Add(Ones, Twos), itor(3), 0, "Add Ones Twos"); // 3 3 3 3 3 - a := make([] *rat, N);\ + a := make([]rat, N);\ d := Diff(Ones); - // BUG: want array initializer for i:=0; i < N; i++ { a[i] = itor(int64(i+1)) } checka(d, a, "Diff"); // 1 2 3 4 5 in := Integ(zero, Ones); - // BUG: want array initializer a[0] = zero; // integration constant for i:=1; i < N; i++ { a[i] = i2tor(1, int64(i)) @@ -662,13 +654,11 @@ func main() {\ check(Cmul(neg(one), Twos), itor(-2), 10, "CMul"); // -1 -1 -1 -1 -1 check(Sub(Ones, Twos), itor(-1), 0, "Sub Ones Twos"); // -1 -1 -1 -1 -1 m := Mul(Ones, Ones); - // BUG: want array initializer for i:=0; i < N; i++ { a[i] = itor(int64(i+1)) } checka(m, a, "Mul"); // 1 2 3 4 5 e := Exp(Ones); - // BUG: want array initializer a[0] = itor(1);\ a[1] = itor(1);\ a[2] = i2tor(3,2);\ @@ -681,7 +671,6 @@ func main() {\ a[9] = i2tor(4596553,362880);\ checka(e, a, "Exp"); // 1 1 3/2 13/6 73/24 at := Integ(zero, MonSubst(Ones, neg(one), 2)); - // BUG: want array initializer for c, i := 1, 0; i < N; i++ { if i%2 == 0 { a[i] = zero @@ -693,7 +682,6 @@ func main() {\ checka(at, a, "ATan"); // 0 -1 0 -1/3 0 -1/5 /* t := Revert(Integ(zero, MonSubst(Ones, neg(one), 2))); - // BUG: want array initializer a[0] = zero;\ a[1] = itor(1);\ a[2] = zero;\ @@ -707,5 +695,4 @@ func main() {\ checka(t, a, "Tan"); // 0 1 0 1/3 0 2/15 */ } - sys.Exit(0); // BUG: force waiting goroutines to exit }
コアとなるコードの解説
上記の変更は、Go言語の設計思想とイディオムに沿ったコードの改善を明確に示しています。
-
item型エイリアスの削除とrat型の直接利用: これは、Goの型システムをより直接的に利用し、コードの冗長性を排除する典型的なリファクタリングです。rat型が値として扱われるようになったことで、*ratへのポインタを指すitemという中間的な型は不要になりました。これにより、コードはより簡潔になり、rat型が直接的にデータの意味を表すようになりました。 -
ratメソッドのレシーバ変更 (*ratからratへ):rat構造体は非常に小さいため、ポインタを介してアクセスするよりも、値としてコピーして渡す方が効率的であると判断された可能性があります。値レシーバを使用することで、メソッド内でレシーバの値を変更しても元の値には影響しないため、意図しない副作用を防ぎ、コードの予測可能性を高めます。これは、Goにおける「小さな構造体は値渡し、大きな構造体や変更が必要な場合はポインタ渡し」という一般的なプラクティスに沿ったものです。 -
チャネルの型変更 (
itemからratへ):item型が削除されたことに伴い、チャネルの型もchan itemからchan ratに変更されました。これにより、チャネルを介して送受信されるデータがrat型の値であることが明確になり、型の一貫性が保たれます。 -
getn関数の引数変更と内部実装の簡略化:getn関数から冗長なn int引数を削除し、len(in)で動的に長さを取得するようにしたことは、関数のシグネチャを簡潔にし、呼び出し側での引数指定ミスを防ぐための改善です。これは、Goの関数設計において、引数を最小限に抑え、必要な情報は内部で取得するという原則に合致しています。 -
クロージャの引数削除: Goのクロージャは、定義されたスコープの変数を自動的にキャプチャする能力を持っています。この変更は、その能力を最大限に活用し、ゴルーチン内で実行される無名関数に外部の変数を明示的に引数として渡す必要がないことを示しています。これにより、コードがより簡潔になり、Goのクロージャのイディオムに沿った記述が可能になります。
-
BUGコメントとsys.Exit(0)の削除: これらの削除は、Go言語の進化と成熟を象徴しています。BUGコメントは、初期のGoにおける機能の不足や設計上の課題を示していましたが、それらが解決されたことで不要になりました。sys.Exit(0)の削除は、Goランタイムがゴルーチンの終了を適切に管理するようになったことを示しており、明示的な終了処理が不要になったことで、よりクリーンなコード記述が可能になりました。
これらの変更は全体として、Go言語のコードベースをよりGoらしく、より効率的で、より保守しやすいものにするための継続的な努力の一環として行われたものです。
関連リンク
- A Tour of Go - Concurrency
- Effective Go - Concurrency
- Go by Example: Channels
- Go by Example: Closures
- Go言語におけるレシーバの選択 (値レシーバ vs ポインタレシーバ) (Go公式ドキュメントのEffective Goより)
参考にした情報源リンク
- Go言語の公式ドキュメント
- GitHubのgolang/goリポジトリ
- Squinting at Power Series by Doug McIlroy (
powser2.goのコメントで参照されている冪級数に関する論文) - Go言語のレシーバに関する一般的な解説記事 (Web検索による)
- Go言語のクロージャに関する一般的な解説記事 (Web検索による)