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

[インデックス 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までであることを示します。

しかし、多くのユースケースでは、ユーザーはインデックスのペアではなく、実際にマッチした部分文字列そのものを直接取得したいと考えます。また、単に文字列が正規表現にマッチするかどうか(真偽値)だけを知りたい場合も多く、そのためだけにインデックスのスライスをチェックするのは冗長でした。

このコミットは、これらのニーズに応えるため、以下の機能を追加することで正規表現インターフェースを「よりリッチ」にすることを目指しました。

  1. シンプルな真偽値マッチング: 文字列が正規表現にマッチするかどうかをbool値で直接返す機能。
  2. 文字列のベクター(スライス): マッチした部分文字列を[]stringとして直接返す機能。これにより、インデックスから手動で部分文字列を抽出する手間が省けます。
  3. グローバルな真偽値関数: 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.gosrc/lib/regexp/all_test.goの2つのファイルが変更されています。

src/lib/regexp/regexp.goの変更点

  1. 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として返します。
  2. 新しいメソッドの実装: *RE型(Regexpインターフェースの実装)に、上記で追加されたMatchMatchStringsメソッドの実装が追加されました。

    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として構築して返します。
  3. グローバルな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メソッドを呼び出しています。これにより、簡単な真偽値チェックのためにCompileMatchを個別に呼び出す手間が省けます。

src/lib/regexp/all_test.goの変更点

新しい機能の導入に伴い、テストファイルも大幅に更新されました。

  1. 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++が正しいです。これはコミット後の修正か、テストコードの初期段階での誤りと考えられます。

  2. 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
    }
    
  3. 既存テスト関数のリファクタリングと新規テストの追加:

    • 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メソッド(正規表現のマッチングを実行し、マッチした部分のインデックスペアを返す内部関数)の結果を利用しています。DoExecutenilでない(つまりマッチが見つかった)場合、返されるインデックススライスの長さは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言語の正規表現パッケージは、より多様なユースケースに対応し、開発者にとってより直感的で効率的なツールとなりました。

関連リンク

参考にした情報源リンク