[インデックス 1413] ファイルの概要
このコミットは、Go言語の正規表現パッケージ(regexp
)に、よりリッチで使いやすいインターフェースを追加することを目的としています。具体的には、正規表現のマッチング結果をより直感的に取得するための新しいメソッドと、簡易的な真偽値判定を行うグローバル関数が導入されました。
コミット
commit 1da03aaef76ab8c216c221ce478dfc10470414c6
Author: Rob Pike <r@golang.org>
Date: Tue Jan 6 13:54:53 2009 -0800
A richer interface for regexps. Simple boolean matcher, a vector of strings rather than
indexes, and a global boolean function for quick action.
R=rsc
DELTA=152 (127 added, 12 deleted, 13 changed)
OCL=22140
CL=22142
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1da03aaef76ab8c216c221ce478dfc10470414c6
元コミット内容
正規表現のためのよりリッチなインターフェース。シンプルな真偽値マッチング、インデックスではなく文字列のベクター(スライス)、そして素早いアクションのためのグローバルな真偽値関数。
変更の背景
このコミットが行われた2009年当時、Go言語はまだ初期段階にあり、標準ライブラリのAPIは活発に開発されていました。正規表現パッケージも例外ではなく、より実用的な機能が求められていました。
以前のregexp
パッケージでは、正規表現のマッチング結果は主にExecute
メソッドによって[]int
(整数のスライス)として返されていました。この[]int
は、マッチした部分文字列の開始・終了インデックスのペアを表していました。例えば、[0, 5, 7, 10]
のような結果は、最初のマッチがインデックス0から5まで、次のマッチがインデックス7から10までであることを示します。
しかし、多くのユースケースでは、ユーザーはインデックスのペアではなく、実際にマッチした部分文字列そのものを直接取得したいと考えます。また、単に文字列が正規表現にマッチするかどうか(真偽値)だけを知りたい場合も多く、そのためだけにインデックスのスライスをチェックするのは冗長でした。
このコミットは、これらのニーズに応えるため、以下の機能を追加することで正規表現インターフェースを「よりリッチ」にすることを目指しました。
- シンプルな真偽値マッチング: 文字列が正規表現にマッチするかどうかを
bool
値で直接返す機能。 - 文字列のベクター(スライス): マッチした部分文字列を
[]string
として直接返す機能。これにより、インデックスから手動で部分文字列を抽出する手間が省けます。 - グローバルな真偽値関数:
regexp.Compile
を介さずに、単一の関数呼び出しで正規表現の真偽値マッチングを行える簡易的な機能。
これらの追加により、regexp
パッケージはより使いやすく、一般的な正規表現の利用パターンに適合するようになりました。
前提知識の解説
このコミットの変更内容を理解するためには、以下の知識が役立ちます。
- 正規表現 (Regular Expression): 特定のパターンを持つ文字列を検索、置換、抽出するための強力なツール。
^
(行頭)、$
(行末)、.
(任意の一文字)、*
(0回以上の繰り返し)、+
(1回以上の繰り返し)、?
(0回または1回の繰り返し)、[]
(文字クラス)、()
(グループ化)などの特殊文字や構文を用いてパターンを定義します。 - Go言語のインターフェース (Interface): Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義する型です。特定のインターフェースを実装する型は、そのインターフェースが定義するすべてのメソッドを持つ必要があります。これにより、異なる具体的な型に対して共通の振る舞いを定義し、ポリモーフィズムを実現できます。
- Go言語のスライス (Slice): Go言語の組み込み型で、同じ型の要素のシーケンスを表します。配列とは異なり、長さが動的に変化します。
[]int
は整数のスライス、[]string
は文字列のスライスを表します。 - Go言語の
regexp
パッケージ: Goの標準ライブラリに含まれる正規表現エンジン。regexp.Compile
関数で正規表現パターンをコンパイルし、Regexp
型のインスタンスを生成してマッチング操作を行います。 - Go言語の
testing
パッケージ: Goの標準ライブラリに含まれるテストフレームワーク。go test
コマンドで実行され、テスト関数はTest
で始まる名前を持ち、*testing.T
型の引数を取ります。
技術的詳細
このコミットでは、src/lib/regexp/regexp.go
とsrc/lib/regexp/all_test.go
の2つのファイルが変更されています。
src/lib/regexp/regexp.go
の変更点
-
Regexp
インターフェースの拡張: 既存のRegexp
インターフェースに、以下の2つの新しいメソッドが追加されました。export type Regexp interface { Execute(s string) []int; Match(s string) bool; // 新規追加 MatchStrings(s string) []string; // 新規追加 }
Match(s string) bool
: 指定された文字列s
が正規表現にマッチするかどうかを真偽値で返します。MatchStrings(s string) []string
: 指定された文字列s
が正規表現にマッチした場合、マッチした部分文字列のリストを[]string
として返します。
-
新しいメソッドの実装:
*RE
型(Regexp
インターフェースの実装)に、上記で追加されたMatch
とMatchStrings
メソッドの実装が追加されました。func (re *RE) Match(s string) bool { return len(re.DoExecute(s, 0)) > 0 } func (re *RE) MatchStrings(s string) []string { r := re.DoExecute(s, 0); if r == nil { return nil } a := new([]string, len(r)/2); for i := 0; i < len(r); i += 2 { a[i/2] = s[r[i] : r[i+1]] } return a }
Match
メソッドは、内部的に既存のDoExecute
メソッドを呼び出し、その結果(インデックスのスライス)の長さが0より大きいかどうかで真偽値を判断します。MatchStrings
メソッドもDoExecute
を呼び出し、得られたインデックスのペアを使って元の文字列から部分文字列を抽出し、[]string
として構築して返します。
-
グローバルな
Match
関数の追加: 正規表現のコンパイルとマッチングを一度に行う、簡易的なグローバル関数Match
が追加されました。export func Match(pattern string, s string) (matched bool, error *os.Error) { re, err := Compile(pattern); if err != nil { return false, err } return re.Match(s), nil }
この関数は、正規表現パターンと対象文字列を受け取り、マッチしたかどうかを真偽値で直接返します。内部的には
Compile
関数で正規表現をコンパイルし、その結果のRegexp
インスタンスのMatch
メソッドを呼び出しています。これにより、簡単な真偽値チェックのためにCompile
とMatch
を個別に呼び出す手間が省けます。
src/lib/regexp/all_test.go
の変更点
新しい機能の導入に伴い、テストファイルも大幅に更新されました。
-
PrintStrings
関数の追加:[]string
型のマッチ結果をテスト出力用に整形して表示するためのヘルパー関数が追加されました。func PrintStrings(t *testing.T, m []string) { l := len(m); if l == 0 { t.Log("\t<no match>"); } else { for i := 0; i < l; i++ { // 修正: i = i+2 から i++ に変更 t.Logf("\t%q", m[i]) } } }
注: 元のコミットでは
i = i+2
となっていましたが、これはMatchStrings
が返すのが部分文字列そのものであり、インデックスペアではないため、i++
が正しいです。これはコミット後の修正か、テストコードの初期段階での誤りと考えられます。 -
EqualStrings
関数の追加: 2つの[]string
スライスが等しいかどうかを比較するためのヘルパー関数が追加されました。これはMatchStrings
のテストで結果の検証に使用されます。func EqualStrings(m1, m2 []string) bool { l := len(m1); if l != len(m2) { return false } for i := 0; i < l; i++ { if m1[i] != m2[i] { return false } // 修正: m1[i] != m2[i] のみで十分 } return true }
-
既存テスト関数のリファクタリングと新規テストの追加:
MatchTest
関数が、新しいRegexp.Match
メソッドのテスト用に変更されました。ExecuteTest
関数が、既存のRegexp.Execute
メソッドのテスト用にリファクタリングされました。TestExecute
関数が追加され、Execute
メソッドのテストを実行します。MatchStringsTest
関数が追加され、Regexp.MatchStrings
メソッドのテストロジックをカプセル化します。TestMatchStrings
関数が追加され、MatchStrings
メソッドのテストを実行します。MatchFunctionTest
関数が追加され、グローバルなMatch
関数のテストロジックをカプセル化します。TestMatchFunction
関数が追加され、グローバルなMatch
関数のテストを実行します。Tester
構造体のテストデータに、マッチしないケース(Tester{
x,\t"y",\tVec{} }
)が追加されました。
これらのテストの追加と修正により、新しい正規表現インターフェースが正しく機能することが保証されます。
コアとなるコードの変更箇所
src/lib/regexp/regexp.go
// Public interface has only execute functionality (not yet implemented)
export type Regexp interface {
Execute(s string) []int;
Match(s string) bool; // 追加
MatchStrings(s string) []string; // 追加
}
// ... (中略) ...
func (re *RE) Match(s string) bool {
return len(re.DoExecute(s, 0)) > 0
}
func (re *RE) MatchStrings(s string) []string {
r := re.DoExecute(s, 0);
if r == nil {
return nil
}
a := new([]string, len(r)/2);
for i := 0; i < len(r); i += 2 {
a[i/2] = s[r[i] : r[i+1]]
}
return a
}
// Exported function for simple boolean check. Anything more fancy
// needs a call to Compile.
export func Match(pattern string, s string) (matched bool, error *os.Error) {
re, err := Compile(pattern);
if err != nil {
return false, err
}
return re.Match(s), nil
}
src/lib/regexp/all_test.go
// ... (中略) ...
func PrintStrings(t *testing.T, m []string) {
l := len(m);
if l == 0 {
t.Log("\t<no match>");
} else {
for i := 0; i < l; i++ {
t.Logf("\t%q", m[i])
}
}
}
func EqualStrings(m1, m2 []string) bool {
l := len(m1);
if l != len(m2) {
return false
}
for i := 0; i < l; i++ {
if m1[i] != m2[i] {
return false
}
}
return true
}
// ... (中略) ...
func MatchTest(t *testing.T, expr string, str string, match []int) {
re := CompileTest(t, expr, nil);
if re == nil {
return
}
m := re.Match(str); // re.Matchを呼び出すように変更
if m != (len(match) > 0) {
t.Error("Match failure on `", expr, "` matching `", str, "`:", m, "should be", len(match) > 0);
}
}
export func TestMatch(t *testing.T) {
for i := 0; i < len(matches); i++ {
test := &matches[i];
MatchTest(t, test.re, test.text, test.match)
}
}
func MatchStringsTest(t *testing.T, expr string, str string, match []int) {
re := CompileTest(t, expr, nil);
if re == nil {
return
}
strs := new([]string, len(match)/2);
for i := 0; i < len(match); i++ {
strs[i/2] = str[match[i] : match[i+1]]
}
m := re.MatchStrings(str); // re.MatchStringsを呼び出す
if !EqualStrings(m, strs) {
t.Error("MatchStrings failure on `", expr, "` matching `", str, "`:");
PrintStrings(t, m);
t.Log("should be:");
PrintStrings(t, strs);
}
}
export func TestMatchStrings(t *testing.T) {
for i := 0; i < len(matches); i++ {
test := &matches[i];
MatchStringsTest(t, test.re, test.text, test.match) // MatchTestからMatchStringsTestに変更
}
}
func MatchFunctionTest(t *testing.T, expr string, str string, match []int) {
m, err := Match(expr, str); // グローバルなMatch関数を呼び出す
if err == nil {
return
}
if m != (len(match) > 0) {
t.Error("function Match failure on `", expr, "` matching `", str, "`:", m, "should be", len(match) > 0);
}
}
export func TestMatchFunction(t *testing.T) {
for i := 0; i < len(matches); i++ {
test := &matches[i];
MatchFunctionTest(t, test.re, test.text, test.match)
}
}
コアとなるコードの解説
このコミットの核心は、正規表現のマッチング結果の利用方法を多様化し、開発者の利便性を向上させる点にあります。
Regexp
インターフェースの拡張と実装
-
Match(s string) bool
: このメソッドは、正規表現が文字列s
に少なくとも1回マッチするかどうかをtrue
またはfalse
で返します。実装は非常にシンプルで、既存のDoExecute
メソッド(正規表現のマッチングを実行し、マッチした部分のインデックスペアを返す内部関数)の結果を利用しています。DoExecute
がnil
でない(つまりマッチが見つかった)場合、返されるインデックススライスの長さは0より大きくなるため、len(re.DoExecute(s, 0)) > 0
という条件で真偽値を判定しています。これは、単にマッチの有無だけを知りたい場合に非常に効率的です。 -
MatchStrings(s string) []string
: このメソッドは、正規表現が文字列s
にマッチした場合、マッチした部分文字列そのものを[]string
(文字列のスライス)として返します。例えば、正規表現がa(b+)c
で文字列がabbbc
の場合、Execute
は[0, 5, 1, 4]
のようなインデックスを返すかもしれませんが、MatchStrings
は["abbbc", "bbb"]
のような結果を返します。 実装では、まずDoExecute
を呼び出してインデックスペアを取得します。その後、このインデックスペアをループで処理し、元の文字列s
からs[r[i] : r[i+1]]
のようにスライス操作を行うことで、対応する部分文字列を抽出しています。これにより、開発者はインデックス計算の手間なく、直接マッチした文字列を利用できるようになります。
グローバルなMatch
関数
func Match(pattern string, s string) (matched bool, error *os.Error)
: この関数は、正規表現パターンと対象文字列を直接引数として受け取り、マッチの有無を真偽値で返します。これは、一度きりの簡単な正規表現チェックを行いたい場合に特に便利です。通常、正規表現を使用するにはまずregexp.Compile
でパターンをコンパイルし、その結果得られたRegexp
オブジェクトのメソッドを呼び出す必要があります。しかし、このグローバルなMatch
関数を使えば、コンパイルとマッチングを1つの関数呼び出しで完結させることができます。内部的にはCompile
を呼び出し、その結果のRegexp
オブジェクトのMatch
メソッドを利用しています。エラーハンドリングも組み込まれており、不正な正規表現パターンが指定された場合にはエラーを返します。
これらの変更により、Go言語の正規表現パッケージは、より多様なユースケースに対応し、開発者にとってより直感的で効率的なツールとなりました。
関連リンク
- Go言語の正規表現パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/regexp
- Go言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master?after=1da03aaef76ab8c216c221ce478dfc10470414c6+34&branch=master
- 正規表現の基本 (Wikipedia): https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE