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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおいて、テンプレートの if-else 構文に else if のサポートを追加するものです。具体的には、パーサーが {{if A}}a{{else if B}}b{{end}} のような記述を、内部的に {{if A}}a{{else}}{{if B}}b{{end}}{{end}} のようにネストされた if-else 構造として解釈するように変更されています。これにより、テンプレートの記述がより簡潔になり、可読性が向上します。

コミット

  • コミットハッシュ: 37cee77ac681654a20939faa047f11b308908902
  • Author: Rob Pike r@golang.org
  • Date: Wed Aug 28 14:43:56 2013 +1000

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

https://github.com/golang/go/commit/37cee77ac681654a20939faa047f11b308908902

元コミット内容

    text/template: allow {{else if ... }} to simplify if chains
    The method is simple: the parser just parses
    
            {{if A}}a{{else if B}}b{{end}}
    
    to the same tree that would be produced by
    
            {{if A}}a{{else}}{{if B}}b{{end}}{{end}}
    
    Thus no changes are required in text/template itself
    or in html/template, only in text/template/parse.
    
    Fixes #6085
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/13327043

変更の背景

この変更の背景には、Goの text/template パッケージにおける if-else 構文の冗長性がありました。従来の text/template では、複数の条件を連鎖させる場合、以下のようにネストされた if-else を記述する必要がありました。

{{if condition1}}
    // do something
{{else}}
    {{if condition2}}
        // do something else
    {{else}}
        {{if condition3}}
            // do yet another thing
        {{end}}
    {{end}}
{{end}}

このような記述は、条件が増えるにつれてインデントが深くなり、テンプレートの可読性と保守性を著しく低下させます。他の多くのプログラミング言語やテンプレートエンジンでは else if 構文が一般的にサポートされており、これにより条件の連鎖をよりフラットかつ簡潔に記述できます。

このコミットは、この問題を解決し、ユーザーがより自然で読みやすい else if 構文を使用できるようにすることを目的としています。コミットメッセージにある Fixes #6085 は、この機能追加が特定の課題(おそらくGitHubのIssueトラッカーで報告されたもの)に対応していることを示唆しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と text/template パッケージの内部動作に関する知識が必要です。

  1. Go言語の text/template パッケージ:

    • Go言語に組み込まれているテキストテンプレートエンジンです。
    • データ構造をテキスト出力に変換するために使用されます。
    • {{...}} で囲まれたアクション(制御構造、データアクセスなど)と、それ以外のリテラルテキストで構成されます。
    • 主な制御構造には ifrangewith などがあります。
  2. テンプレートのパース(Parsing):

    • テンプレートエンジンは、まず与えられたテンプレート文字列を解析(パース)し、内部的な表現(通常は抽象構文木、AST)に変換します。
    • このASTは、テンプレートの構造とロジックを表現するノードのツリーです。
    • text/template パッケージでは、text/template/parse サブパッケージがこのパース処理を担当します。
  3. 抽象構文木(Abstract Syntax Tree, AST):

    • ソースコードやテンプレートの構造を木構造で表現したものです。
    • 各ノードは、プログラムの構成要素(変数、演算子、制御構造など)に対応します。
    • テンプレートエンジンは、このASTをトラバースして最終的な出力を生成します。
  4. if / else 制御構造:

    • text/template における if アクションは、パイプライン(式)の評価結果に基づいて条件分岐を行います。
    • {{if pipeline}} T1 {{else}} T0 {{end}} の形式で記述され、pipeline が真と評価されれば T1 が実行され、偽と評価されれば T0 が実行されます。
  5. 字句解析(Lexing)と構文解析(Parsing):

    • 字句解析(Lexing): テンプレート文字列をトークン(意味を持つ最小単位、例: if キーワード、else キーワード、識別子、区切り文字など)のストリームに分解するプロセス。text/template/parse パッケージでは item 型がトークンを表します。
    • 構文解析(Parsing): トークンのストリームを文法規則に従って解析し、ASTを構築するプロセス。このコミットの変更は、主にこの構文解析の段階で行われます。

このコミットの核心は、else if という新しい構文を導入するのではなく、既存のパーサーが else if を検出した際に、それをネストされた if-else 構造としてASTに変換するという点にあります。これにより、text/templatehtml/template の実行エンジン自体には変更を加える必要がなく、パーサーのみの変更で機能を実現しています。

技術的詳細

このコミットの技術的詳細は、text/template/parse パッケージにおける構文解析ロジックの変更に集約されます。

従来の text/template のパーサーは、{{else}} の後に続く内容を、現在の if ブロックの代替(else節)としてのみ解釈していました。しかし、この変更では、{{else}} の直後に if キーワードが続く場合(すなわち {{else if ...}} の形式)、パーサーはこれを特別なケースとして扱います。

具体的には、パーサーは {{else if ...}} を検出すると、以下のように動作します。

  1. else トークンを処理します。
  2. 次に if トークンが続くことを検出します。
  3. この if を、現在の else 節の内部に新しい if ブロックとしてパースします。
  4. この新しい if ブロックが終了する {{end}} までをパースし、その後の {{end}} は、外側の if ブロックの終了として扱われます。

これにより、{{if A}}a{{else if B}}b{{end}} というテンプレート文字列は、パーサーによって以下のようなASTに変換されます。

IfNode (A)
  - ListNode (a)
  - ElseNode
    - IfNode (B)
      - ListNode (b)
      - (no else)

このASTは、手動で {{if A}}a{{else}}{{if B}}b{{end}}{{end}} と記述した場合に生成されるASTと全く同じになります。したがって、text/templatehtml/template の実行エンジンは、この新しい構文を特別に意識することなく、既存のロジックで正しくテンプレートを実行できます。

このアプローチの利点は以下の通りです。

  • 後方互換性: 既存のテンプレートや実行エンジンに影響を与えません。
  • コードの簡潔さ: テンプレートユーザーは else if を使用してコードを簡潔に記述できます。
  • 実装の単純さ: 変更がパーサー層に限定されるため、実装が比較的単純です。

コミットメッセージには「TODO: Should we allow else-if in with and range?」というコメントがあり、これは将来的に withrange アクションでも同様の else if 構文をサポートする可能性を示唆しています。しかし、このコミットでは if アクションにのみ適用されています。

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

このコミットによる主要なコード変更は、src/pkg/text/template/parse/parse.go ファイルに集中しています。

  1. src/pkg/text/template/parse/parse.go:

    • parseControl 関数のシグネチャが変更され、allowElseIf bool という新しい引数が追加されました。この引数は、現在の制御構造が else if を許可するかどうかを制御します。
    • parseControl 関数内で、nodeElse のケースに allowElseIftrue の場合に else if を処理するロジックが追加されました。具体的には、t.peek().typ == itemIfelse の直後に if が続くかをチェックし、続く場合は新しい if ノードを elseList に追加します。
    • ifControl 関数が newIf(t.parseControl(true, "if")) を呼び出すように変更され、if アクションが else if を許可するように設定されました。
    • rangeControlwithControl 関数は newRange(t.parseControl(false, "range")) および newWith(t.parseControl(false, "with")) を呼び出すように変更され、これらが else if を許可しないように設定されました。
    • elseControl 関数に、{{else if ...}} のケースを特別に処理するためのロジックが追加されました。t.peekNonSpace().typ == itemIfelse の直後に if が続くかをチェックし、その場合は newElse を返すことで、パーサーが else の後に続く if を適切に処理できるようにします。
  2. src/pkg/text/template/doc.go:

    • if アクションのドキュメントに、{{if pipeline}} T1 {{else if pipeline}} T0 {{end}} の新しい構文とその解釈に関する説明が追加されました。
  3. src/pkg/text/template/exec_test.go:

    • if else ifif else chain の新しいテストケースが追加され、else if 構文が正しく機能することを確認しています。
  4. src/pkg/text/template/parse/parse_test.go:

    • if with else ifif else chain の新しいパーステストケースが追加され、else if 構文が正しくASTに変換されることを確認しています。
    • extra end after if というエラーケースのテストも追加され、{{if .X}}a{{else if .Y}}b{{end}}{{end}} のように余分な {{end}} がある場合にエラーが報告されることを確認しています。

コアとなるコードの解説

src/pkg/text/template/parse/parse.go の変更点

最も重要な変更は parseControl 関数と elseControl 関数にあります。

parseControl 関数の変更

// func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { // Old signature
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { // New signature
    // ...
    switch next.Type() {
    case nodeEnd: //done
    case nodeElse:
        if allowElseIf { // 新しい allowElseIf フラグ
            // Special case for "else if". If the "else" is followed immediately by an "if",
            // the elseControl will have left the "if" token pending. Treat
            //  {{if a}}_{{else if b}}_{{end}}
            // as
            //  {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
            // To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
            // is assumed. This technique works even for long if-else-if chains.
            // TODO: Should we allow else-if in with and range?
            if t.peek().typ == itemIf { // else の直後に if が続くかチェック
                t.next() // Consume the "if" token.
                elseList = newList(next.Position())
                elseList.append(t.ifControl()) // 新しい if ブロックを elseList に追加
                // Do not consume the next item - only one {{end}} required.
                break
            }
        }
        elseList, next = t.itemList()
        // ...
    }
    // ...
}
  • parseControl は、if, range, with のような制御構造をパースする汎用関数です。
  • allowElseIf 引数が追加され、現在の制御構造が else if を許可するかどうかを明示的に指定できるようになりました。if アクションの場合は truerangewith の場合は false が渡されます。
  • nodeElse のケースで、allowElseIftrue かつ else の直後に if トークン (itemIf) が続く場合、パーサーは t.next()if トークンを消費し、t.ifControl() を呼び出して新しい if ブロックをパースします。この新しい if ブロックは、現在の else 節のコンテンツとして elseList に追加されます。
  • このロジックにより、{{else if ...}} は内部的に {{else}}{{if ...}}{{end}} と同じAST構造に変換されます。

elseControl 関数の変更

func (t *Tree) elseControl() Node {
    // Special case for "else if".
    peek := t.peekNonSpace()
    if peek.typ == itemIf { // else の直後に if が続くかチェック
        // We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
        return newElse(peek.pos, t.lex.lineNumber())
    }
    return newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
}
  • elseControl{{else}} アクションをパースする関数です。
  • t.peekNonSpace() を使用して、else の直後に空白以外のトークンが何かを調べます。
  • もしそれが itemIf であれば、これは {{else if ...}} のパターンであることを意味します。この場合、newElse を返すことで、パーサーは else ノードを生成し、その後の ifparseControl 関数内のロジックによって適切に処理されるようになります。これにより、else if が単一の else ノードとして扱われるのではなく、else の後に続く if として解釈される道筋が作られます。

src/pkg/text/template/doc.go の変更点

ドキュメントの追加は、この新機能の利用方法をユーザーに伝える上で非常に重要です。

 	{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
 		To simplify the appearance of if-else chains, the else action
 		of an if may include another if directly; the effect is exactly
 		the same as writing
 			{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

この説明は、else if が単なる糖衣構文(syntactic sugar)であり、内部的にはネストされた if と同じであることを明確に示しています。

テストケースの追加

exec_test.goparse_test.go に追加されたテストケースは、この機能が期待通りに動作することを確認します。特に if else chain のテストは、複数の else if が連鎖する場合の正しい動作を検証しています。

// src/pkg/text/template/exec_test.go
{"if else if", "{{if false}}FALSE{{else if true}}TRUE{{end}}", "TRUE", tVal, true},
{"if else chain", "{{if eq 1 3}}1{{else if eq 2 3}}2{{else if eq 3 3}}3{{end}}", "3", tVal, true},

// src/pkg/text/template/parse/parse_test.go
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
    `{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+\", noError,
    `"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},

これらのテストは、else if が正しく評価され、またパース時に期待されるAST構造に変換されることを保証します。

関連リンク

参考にした情報源リンク

  • コミットメッセージ: 37cee77ac681654a20939faa047f11b308908902
  • Go言語 text/template パッケージのドキュメント
  • Go言語 text/template/parse パッケージのソースコード