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

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

このコミットは、Go言語の初期の標準ライブラリの一部であった src/lib/container/array/array.go およびそのテストファイル src/lib/container/array/array_test.go に変更を加えています。具体的には、Array 型に要素を巡回するための Do メソッドが追加され、その動作を検証するためのテストが記述されています。

コミット

このコミットは、Go言語の container/array パッケージに、配列の各要素に対して指定された関数を適用する「ビジターメソッド」を追加するものです。これは、コレクションの要素を反復処理するための一般的なパターンを実装したものであり、当時のGo言語におけるコレクション操作の進化の一端を示しています。

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

https://github.com/golang/go/commit/39d05ae808849f09cceb07b7970e6f493a0822e2

元コミット内容

add a trivial visitor method, just for fun

R=gri
DELTA=31  (30 added, 1 deleted, 0 changed)
OCL=24568
CL=24575
---
 src/lib/container/array/array.go      |  7 +++++++
 src/lib/container/array/array_test.go | 24 +++++++++++++++++++++++-
 2 files changed, 30 insertions(+), 1 deletion(-)

変更の背景

このコミットは2009年に行われており、Go言語がまだ開発の初期段階にあった時期のものです。当時のGo言語には、現在のような組み込みのスライス([]T)やマップ(map[K]V)が完全に成熟しておらず、container パッケージ群がより汎用的なデータ構造を提供していました。container/array パッケージは、その名の通り配列のようなデータ構造を抽象化したものであり、その要素を効率的に処理するためのメカニズムが求められていました。

Do メソッドの追加は、コレクションの各要素に対して特定の操作を実行するという一般的な要件を満たすためのものです。これは、関数型プログラミングにおける forEachmap のような操作の初期的な実装と見なすことができます。コミットメッセージにある「just for fun」という記述は、当時のGo言語の設計者が、言語の表現力やユーティリティを試行錯誤していた様子をうかがわせます。

また、この変更は、Go言語がどのようにして汎用的なデータ構造とそれらを操作するイディオムを確立していったかを示す歴史的な一例でもあります。最終的に container/array パッケージはGo言語の標準ライブラリから削除され、より強力で柔軟な組み込みのスライスがその役割を担うことになりますが、このコミットはGo言語の進化の過程における重要なステップの一つと言えます。

前提知識の解説

1. Go言語の初期のコレクション (container/array パッケージ)

現在のGo言語では、スライス([]T)が最も一般的で強力な動的配列の実装ですが、Go言語の初期には container/array パッケージが存在しました。これは、interface{} 型(当時の Element 型に相当)を要素として持つ汎用的な配列を提供し、動的なサイズ変更や要素へのアクセスなどの機能を持っていました。このパッケージは、Go言語が組み込みのスライスを最適化し、その機能が成熟するまでの過渡期的な役割を果たしました。

2. Goにおけるメソッドとレシーバ

Go言語では、構造体(struct)や任意の型にメソッドを定義できます。メソッドは、特定の型の値(レシーバ)に対して操作を行う関数です。レシーバは、値レシーバ(func (p MyType) MethodName(...))とポインタレシーバ(func (p *MyType) MethodName(...))の2種類があります。このコミットで追加された Do メソッドはポインタレシーバ (p *Array) を持っており、これは Array のインスタンス自体を変更する可能性のある操作に適しています(ただし、今回の Do メソッドは要素の変更は行いませんが、引数として渡される関数 fArray の内部状態を変更する可能性についてコメントで注意喚起されています)。

3. 関数型引数(高階関数)

Go言語は関数を第一級オブジェクトとして扱います。これは、関数を変数に代入したり、関数の引数として渡したり、関数の戻り値として返したりできることを意味します。Do メソッドは func(elem Element) という関数を引数として受け取ります。このような、関数を引数として受け取ったり、関数を戻り値として返したりする関数を「高階関数」と呼びます。これにより、柔軟で再利用可能なコードを書くことが可能になります。

4. Visitorパターン

Visitorパターンは、オブジェクト構造の要素に対して実行される操作を、その要素のクラスを変更することなく定義できるデザインパターンです。このパターンでは、操作をカプセル化する「ビジター」オブジェクトを定義し、オブジェクト構造の各要素がビジターを受け入れるメソッド(accept メソッドなど)を提供します。ビジターは、訪問する要素の型に応じて異なる操作を実行します。

このコミットで追加された Do メソッドは、厳密な意味でのVisitorパターン(accept メソッドと Visitor インターフェースを持つ)ではありませんが、その概念、つまり「コレクションの各要素を巡回し、それぞれの要素に対して外部から提供された操作を適用する」という点で「ビジターメソッド」と表現されています。これは、コレクションの内部構造に依存せずに、要素に対する操作を外部から注入できるという点で、Visitorパターンの思想に近いものです。

技術的詳細

追加された Do メソッドは、*Array 型のレシーバを持ち、func(elem Element) 型の関数 f を引数として受け取ります。

func (p *Array) Do(f func(elem Element)) {
	for i := 0; i < len(p.a); i++ {
		f(p.a[i])	// not too safe if f changes the Array
	}
}

このメソッドの動作は以下の通りです。

  1. for i := 0; i < len(p.a); i++Array の内部的な配列 p.a の全要素を、インデックス i を使って先頭から末尾まで順に反復処理します。
  2. f(p.a[i]):現在のインデックス i にある要素 p.a[i] を、引数として渡された関数 f に渡して実行します。これにより、Array の各要素に対して、呼び出し元が定義した任意の操作を適用できます。
  3. // not too safe if f changes the Array:このコメントは非常に重要です。Do メソッドは Array の要素を f に渡しますが、f の内部で Array の構造(例えば、要素の追加や削除、または p.a 自体の変更)が行われた場合、for ループの反復処理が予期せぬ動作をする可能性があることを示唆しています。例えば、fArray から要素を削除した場合、ループのインデックスがずれて要素がスキップされたり、範囲外アクセスが発生したりする可能性があります。このコメントは、このメソッドを使用する開発者に対して、渡す関数の副作用に注意を促しています。

テストファイル src/lib/container/array/array_test.go に追加された TestDo 関数は、この Do メソッドの基本的な動作を検証します。

func TestDo(t *testing.T) {
	const n = 25;
	const salt = 17;
	a := array.NewIntArray(n); // n個の整数を格納するArrayを生成
	for i := 0; i < n; i++ {
		a.Set(i, salt * i); // 各要素に salt * i の値を設定
	}
	count := 0;
	a.Do(
		func(e array.Element) {
			i := e.(int); // Elementをint型に型アサーション
			if i != count*salt {
				t.Error("value at", count, "should be", count*salt, "not", i)
			}
			count++; // 巡回した要素数をカウント
		}
	);
	if count != n {
		t.Error("should visit", n, "values; did visit", count)
	}
}

このテストでは、以下の手順で Do メソッドを検証しています。

  1. n 個(25個)の整数を格納する Array インスタンス a を作成します。
  2. 各要素に salt * isalt は17)という規則的な値を設定します。
  3. count 変数を初期化し、Do メソッドに渡す匿名関数内で、巡回した要素の数と期待される値が一致するかを検証します。
    • 匿名関数内では、Element 型の要素 eint 型に型アサーションしています。
    • i != count*salt の条件で、現在の要素の値が count に基づく期待値と一致するかを確認します。
    • count をインクリメントすることで、巡回順序と要素数が正しいことを確認します。
  4. Do メソッドの実行後、最終的に countn と等しいかを確認し、すべての要素が一度ずつ巡回されたことを検証します。

このテストは、Do メソッドが Array のすべての要素を正しい順序で巡回し、各要素に対して指定された関数を正確に適用することを確認しています。

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

src/lib/container/array/array.go の変更

--- a/src/lib/container/array/array.go
+++ b/src/lib/container/array/array.go
@@ -140,6 +140,13 @@ func (p *Array) Slice(i, j int) *Array {
 }
 
 
+func (p *Array) Do(f func(elem Element)) {
+	for i := 0; i < len(p.a); i++ {
+		f(p.a[i])	// not too safe if f changes the Array
+	}
+}
+
+
 // Convenience wrappers
 
 func (p *Array) Push(x Element) {

src/lib/container/array/array_test.go の変更

--- a/src/lib/container/array/array_test.go
+++ b/src/lib/container/array/array_test.go
@@ -139,7 +139,6 @@ func TestInsertArray(t *testing.T) {
 	verify_pattern(t, a, 8, 1000, 2);
 }
 
-
 func TestSorting(t *testing.T) {
 	const n = 100;
 	a := array.NewIntArray(n);
@@ -148,3 +147,26 @@ func TestSorting(t *t.T) {
 	}
 	if sort.IsSorted(a) { t.Error("not sorted") }
 }
+
+
+func TestDo(t *testing.T) {
+	const n = 25;
+	const salt = 17;
+	a := array.NewIntArray(n);
+	for i := 0; i < n; i++ {
+		a.Set(i, salt * i);
+	}
+	count := 0;
+	a.Do(
+		func(e array.Element) {
+			i := e.(int);
+			if i != count*salt {
+				t.Error("value at", count, "should be", count*salt, "not", i)
+			}
+			count++;
+		}
+	);
+	if count != n {
+		t.Error("should visit", n, "values; did visit", count)
+	}
+}

コアとなるコードの解説

array.go における Do メソッド

Do メソッドは Array 型のポインタレシーバ p を持ち、func(elem Element) 型の関数 f を引数として受け取ります。

func (p *Array) Do(f func(elem Element)) {
	for i := 0; i < len(p.a); i++ { // Arrayの内部配列p.aの全要素をインデックスiで巡回
		f(p.a[i])	// 各要素p.a[i]を引数fに渡して実行。fがArrayを変更すると安全ではない可能性あり
	}
}

このメソッドは、Array が保持するすべての要素に対して、引数 f で指定された操作を順次適用します。これは、コレクションの要素を反復処理し、それぞれにカスタムロジックを適用する一般的なパターンです。コメント // not too safe if f changes the Array は、fArray の基盤となる構造(例えば、要素の追加や削除)を変更した場合に、ループの動作が不安定になる可能性があるという重要な警告です。これは、Go言語における同時変更の安全性や、イテレータの無効化に関する初期の考慮事項を示唆しています。

array_test.go における TestDo 関数

TestDo 関数は、Do メソッドが正しく機能するかを検証するための単体テストです。

func TestDo(t *testing.T) {
	const n = 25; // テスト対象のArrayのサイズ
	const salt = 17; // 要素の値を計算するための定数
	a := array.NewIntArray(n); // n個のint型要素を持つArrayを新規作成
	for i := 0; i < n; i++ {
		a.Set(i, salt * i); // Arrayの各要素に、インデックスiに応じた規則的な値を設定
	}
	count := 0; // Doメソッドが巡回した要素数をカウントする変数
	a.Do( // Doメソッドを呼び出し、匿名関数を引数として渡す
		func(e array.Element) { // Doメソッドによって各要素が渡される匿名関数
			i := e.(int); // Element型をint型に型アサーション(Goの初期のジェネリクス的なアプローチ)
			if i != count*salt { // 現在の要素の値が期待値(count*salt)と異なる場合
				t.Error("value at", count, "should be", count*salt, "not", i) // エラーを報告
			}
			count++; // 巡回した要素数をインクリメント
		}
	);
	if count != n { // Doメソッドがすべての要素を巡回しなかった場合
		t.Error("should visit", n, "values; did visit", count) // エラーを報告
	}
}

このテストは、Do メソッドが Array のすべての要素を、期待される順序で、かつ正しい値で巡回していることを確認します。count 変数と salt を用いた検証は、要素の順序性と値の正確性を同時にチェックする巧妙な方法です。e.(int) のような型アサーションは、Go言語の初期においてジェネリクスがなかった時代に、interface{} を介して汎用的なデータ構造を扱う一般的なパターンでした。

関連リンク

参考にした情報源リンク

  • GitHub: golang/go リポジトリのコミット履歴
  • Go言語の公式ドキュメント
  • Go言語に関する技術ブログや記事 (Go言語の初期の歴史やデザインパターンに関するもの)
  • デザインパターンに関する一般的な知識 (Visitorパターン)