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

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

このコミットは、Go言語の仕様書 doc/go_spec.html に対する更新であり、特に式の評価順序が未規定であるケースについて、より具体的な例を追加することで、仕様の明確化を図っています。これにより、Goプログラマーがコードの挙動をより正確に理解し、未規定の評価順序に依存するようなバグを回避できるよう支援することを目的としています。

コミット

doc/go_spec: 評価順序の未規定ケースに関するより多くの例

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

https://github.com/golang/go/commit/bdac989ef7f7f3223d8dd4928dee3da595260f47

元コミット内容

commit bdac989ef7f7f3223d8dd4928dee3da595260f47
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Jun 11 02:52:07 2013 +0800

    doc/go_spec: more examples for unspecified cases of the evaluation order
    
    R=golang-dev, bradfitz, gri, iant, rsc
    CC=golang-dev
    https://golang.org/cl/7235044

変更の背景

プログラミング言語において、式の評価順序はプログラムの挙動を決定する上で極めて重要です。しかし、全ての評価順序を厳密に規定すると、コンパイラやランタイムの実装が複雑になり、最適化の機会が失われる可能性があります。そのため、多くの言語仕様では、特定のケースにおける評価順序を「未規定 (unspecified)」とすることで、実装の自由度を確保しています。

Go言語も例外ではなく、一部の操作において評価順序が未規定とされています。これは、コンパイラがより効率的なコードを生成するための設計上の選択です。しかし、この「未規定」という概念は、プログラマーにとっては混乱の元となることがあります。特に、評価順序に依存するような副作用を伴う式を記述した場合、異なるコンパイラや異なる実行環境でプログラムの挙動が変わってしまう「未定義動作 (undefined behavior)」につながる可能性があります。

このコミットは、Go言語の仕様書に、評価順序が未規定である具体的なシナリオの例を追加することで、この問題を緩和しようとしています。これにより、プログラマーはどのような場合に評価順序に依存すべきでないかを明確に理解し、より堅牢で移植性の高いコードを書くことができるようになります。

前提知識の解説

評価順序 (Evaluation Order)

評価順序とは、プログラミング言語において、式を構成する各部分(オペランド、関数呼び出しなど)が評価される順番のことです。例えば、a + b * c という式では、b * c が先に評価され、その結果が a と加算される、といった具体的な順序があります。多くの言語では、演算子の優先順位や結合規則によって評価順序が定められています。

未規定の評価順序 (Unspecified Evaluation Order)

未規定の評価順序とは、言語仕様が特定の式の評価順序を明示的に定めていない状態を指します。これは、コンパイラの実装者がその順序を自由に選択できることを意味します。結果として、同じソースコードであっても、異なるコンパイラや異なるコンパイルオプション、あるいは同じコンパイラの異なるバージョンでコンパイルされた場合、評価順序が変わり、プログラムの挙動が異なる可能性があります。

副作用 (Side Effects)

副作用とは、関数や式がその戻り値以外に、プログラムの状態に与える影響のことです。例えば、変数の値を変更する、I/O操作を行う、グローバル変数を更新するなどが副作用にあたります。未規定の評価順序と副作用が組み合わさると、予期せぬ結果を招くことがあります。例えば、f(a) + g(a) のような式で、fg の両方が a の値を変更する副作用を持つ場合、fg のどちらが先に評価されるかによって、最終的な結果が変わってしまう可能性があります。

Go言語の仕様と評価順序

Go言語の仕様では、一部の操作において評価順序が未規定であることが明記されています。これは、コンパイラがより積極的な最適化を行うための設計上のトレードオフです。Goの設計思想として、プログラマーが未規定の評価順序に依存するようなコードを書くことを避けるべきであるという考え方があります。このコミットは、その考え方をより具体的に示すためのものです。

技術的詳細

このコミットは、Go言語の仕様書 doc/go_spec.html の「Order of evaluation」セクションに、評価順序が未規定であるケースを示す新しい例を追加しています。

具体的には、以下の3つの新しい例が追加されました。

  1. スライスリテラルにおける要素の評価順序: x := []int{a, f()} の例では、スライスリテラルの要素 af() の評価順序が未規定であることを示しています。f()a の値を変更する副作用を持つ場合、x の結果が [1, 2] になるか [2, 2] になるかは、af() のどちらが先に評価されるかによって異なります。

  2. マップリテラルにおけるキーと値の評価順序、および重複キーの評価順序: m := map[int]int{a: 1, a: 2} の例では、マップリテラル内で同じキーが複数回出現する場合、どの値が最終的にマップに格納されるか(つまり、重複キーに対する代入の評価順序)が未規定であることを示しています。結果は {2: 1} または {2: 2} のいずれかになります。 m2 := map[int]int{a: f()} の例では、マップリテラルのキー a と値 f() の評価順序が未規定であることを示しています。f()a の値を変更する副作用を持つ場合、m2 の結果が {2: 3} になるか {3: 3} になるかは、キーと値のどちらが先に評価されるかによって異なります。

これらの例は、Go言語のプログラマーが、評価順序が未規定である状況で副作用を伴う式を記述する際に、その挙動が予測不可能になる可能性があることを明確に警告しています。これにより、プログラマーはこのようなコードを避け、より明確で予測可能なコードを書くよう促されます。

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

doc/go_spec.html ファイルの以下の部分が変更されました。

--- a/doc/go_spec.html
+++ b/doc/go_spec.html
@@ -1,6 +1,6 @@
 <!--{
  	"Title": "The Go Programming Language Specification",
-	"Subtitle": "Version of May 31, 2013",
+	"Subtitle": "Version of June 11, 2013",
  	"Path": "/ref/spec"
 }-->
 
@@ -3929,8 +3929,10 @@ of <code>y</code> is not specified.
 
 <pre>
 a := 1
-f := func() int { a = 2; return 3 }\n-x := []int{a, f()}  // x may be [1, 3] or [2, 3]: evaluation order between a and f() is not specified
+f := func() int { a++; return a }
+x := []int{a, f()} // x may be [1, 2] or [2, 2]: evaluation order between a and f() is not specified
+m := map[int]int{a: 1, a: 2} // m may be {2: 1} or {2: 2}: evaluation order between the two map assignments is not specified
+m2 := map[int]int{a: f()} // m2 may be {2: 3} or {3: 3}: evaluation order between the key and the value is not specified
 </pre>
 
 <p>

コアとなるコードの解説

変更されたコードは、Go言語の仕様書に記載されている評価順序の未規定性に関する説明を補強するものです。

元の例では、x := []int{a, f()} のケースで f()a = 2 のように a を直接代入する例が示されていました。これは x[1, 3] または [2, 3] になる可能性を示唆していました。

新しいコミットでは、f := func() int { a++; return a } のように a をインクリメントする副作用を持つ関数 f を導入し、より具体的なシナリオを提示しています。

  1. x := []int{a, f()}:

    • a の初期値は 1
    • f()a をインクリメントしてから a の値を返す。
    • もし a が先に評価されれば x[0]1。その後 f() が評価され a2 になり、f()2 を返すので x[1]2。結果 [1, 2]
    • もし f() が先に評価されれば a2 になり、f()2 を返すので x[1]2。その後 a が評価され x[0]2。結果 [2, 2]
    • この例は、スライスリテラルの要素の評価順序が未規定であることを明確に示しています。
  2. m := map[int]int{a: 1, a: 2}:

    • a の初期値は 1
    • この例は、マップリテラル内で同じキー a が複数回出現する場合の挙動を示しています。Goのマップはキーが一意であるため、同じキーに対する複数の代入は、最後に評価された値が採用されます。しかし、これらの代入が評価される順序は未規定です。
    • もし {a: 1} が先に評価され、次に {a: 2} が評価されれば、最終的に m{2: 2} となる(a の値が 2 になった後)。
    • もし {a: 2} が先に評価され、次に {a: 1} が評価されれば、最終的に m{2: 1} となる。
    • この例は、マップリテラルにおける重複キーの評価順序が未規定であることを示しています。
  3. m2 := map[int]int{a: f()}:

    • a の初期値は 1
    • この例は、マップリテラルのキー a と値 f() の評価順序が未規定であることを示しています。
    • もしキー a が先に評価されれば m2 のキーは 1。その後 f() が評価され a2 になり、f()2 を返すので m2 の値は 2。結果 {1: 2}
    • もし値 f() が先に評価されれば a2 になり、f()2 を返すので m2 の値は 2。その後キー a が評価され m2 のキーは 2。結果 {2: 2}
    • この例は、マップリテラルのキーと値の評価順序が未規定であることを示しています。

これらの新しい例は、Go言語の仕様が意図的に評価順序を未規定としている箇所を、より具体的かつ実践的なシナリオで示しており、プログラマーがこれらの未規定性に依存しないコードを書くことの重要性を強調しています。

関連リンク

参考にした情報源リンク