Go
GunosyDay 15

Go の定数の話

More than 3 years have passed since last update.


はじめに

先月の GoCon 2014 Autumn のキーノートスピーチで Rob Pike 先生が、「Simplicity is Complicated」というタイトルで講演を行いました( @tenntenn さんによるレポート記事 )。

その中で、「Go の定数は、裏では非常に複雑なことをやっている」という話をされていました。そのときに触れられていた、The Go blog の "Constants" という記事を読んだので、それについてまとめます。


TL;DR


  • Go では、異なる型を混ぜた式は禁止し明示的な変換を要求する

  • しかし、定数を毎回変換するのは面倒なので、定数は型を持たないことが可能になっている

  • これによって、Go の定数は、実際の数字のように扱うことができる


参考文献

参考というか、そのままなので、下記を読めば、本記事を読む必要はありません。また、本記事を読んで興味を持った方は、ぜひリンク先を読んでみてください。


Go の定数の型

Go 言語は、ご存知のように静的型付け言語です。さらに、異なる型間の暗黙的な変換を許容していません。たとえば、以下のような式はすべてコンパイルエラーとなります。

var u uint = 1

var i int = 1
var p int32 = 1

u + i // Error: invalid operation: u + i (mismatched types uint and int)
i + p // Error: invalid operation: i + p (mismatched types int and int32)

u + uint(i) // OK
p + int32(i) // OK

(http://play.golang.org/p/AYoAL_h13i)

この制約が導入されたのは、C の暗黙的型変換によって、おおくのバグや移植の困難さが生まれていたからだそうです。

一方、これは新たな不便さを生みます。特に、定数を使うときに、いちいち i=int(0)math.Sqrt(int(2)) と書かないといけないのは、ぞっとしません。

そこで、Go では、定数が untyped であることを許容しています。つまり、以下の定数の型は決まっていない、ということです。変数に代入するときや式の中で型が決定するのです。

const u = 1

const p = 2.0

a := u + 0i // a は `1+0i`
var b int = p // b は `2`

(http://play.golang.org/p/rq2zTgoVIf)


untyped な定数のメリット

このような untyped な定数があると、以下のように書くことができます。

const hello = "Hello, ワールド"

var s string = hello // OK
type MyString string
var ms MyString = hello // OK
ms = s // NG

(http://play.golang.org/p/WO0qsvu7FN)

いかがでしょうか、定数に型がないことによって、MyString のような独自定義型の変数にも代入が可能になっています。もし定数宣言が const hello string = ... のように型指定が必須なら、最後の行のようにコンパイルエラーが起こっていたことでしょう。

この仕組みによって、暗黙的な型変換を抑制しつつ、柔軟性を確保しているのです。


定数の default type

それでは、以下のプログラムを実行した場合、変数 a の型はなにになるでしょうか。また2行目で表示される型はなにになるでしょうか。

a := 2.0

fmt.Printf("%T\n", 'x')

(http://play.golang.org/p/d4ZY5JwHo-)

これらは、定数の default type によって決まります。

Go 言語では、他に型を決定する情報がなにもないときに、定数の default type を型として用います。上記の例の1行目では、変数 a の型は 2.0default type である float64 になります。

また、2行目の例では、fmt.Printf の第2引数以降は interface 型ですが、そこに定数が渡されています。このときも、定数 'x'default type である rune が使われます。

なお、runeint32 のエイリアスなので、このプログラムの実行結果は rune ではなく int32 となります。


定数の精度

それでは、型がない untyped な Go の定数の範囲はいくつでしょうか。言い方を変えると、Go の定数として宣言できる値の大きさはいくらまで許容されるのでしょうか? 32bit? 64bit?

実は、untyped な Go の定数は無限精度になっておりオーバーフローは発生しません。つまり、いくら大きな値や非常に桁数が膨大な浮動小数点を指定しても構わないのです。

*正確には、仕様上の制約がないだけで、実際に記述できる数値の範囲は処理系に依存します。仕様ではある程度の精度が表せることをコンパイラに要求しています。詳細は、定数の仕様を参照してください。

もちろん、実際の型を決定するときには、その型の範囲を逸脱しない必要があります。たとえば、以下のようなプログラムは1行目はコンパイルエラーになりませんが、2行目はコンパイルエラーになります。しかし、3行目は問題ありません。

const Huge = 1e1000

a := Huge // Error: constant 1.00000e+1000 overflows float64
fmt.Println(Huge / 1e999) // `10` が出力される

(http://play.golang.org/p/k95No7HOlq)

一見すると、変数に代入できないなら定数の精度が無限でも無意味に見えますが、上記の例の3行目のように、精度を落とすことなく大きな定数の演算結果を利用できるというメリットがあります。

実際、Go 言語では円周率を以下のように定義しています。これは float64 で表せる精度を超えていますが、これによって、Pi/2 などの演算が高精度で行えるのです。

Pi    = 3.14159265358979323846264338327950288419716939937510582097494459


定数の世界

上で見たように、Go の定数では、型もなく精度も(仕様上は)ありません。これは、つまり私たちが現実の世界で数字を扱うのと同じやりかたです。

つまり Go の数値定数の中では、整数も浮動小数点も複素数も、そして文字コードも、同じ世界の住人となるのです。そして、変数の世界では禁じられている、それらの数値の混在も認められるのです。

たとえば、以下はすべて数値の 1 を表します。

1

1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i

これらは、すべて数値の 1 なので、当然、どうのような数値型にも代入することもできます。

var f float32 = 1

var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = '\x01'
var p uintptr = '\u0001'
var r complex64 = 'b' - 'a'
var b byte = 1.0 + 3i - 3.0i

fmt.Println(f, i, u, c, p, r, b)

(http://play.golang.org/p/D_YUtnhK7j)

少々トリッキーに見えるかもしれませんが、このような挙動は、決して複雑さを導入するためではなく、柔軟性を確保するためです。たとえば、以下のようなコードが、コンパイル可能になっています。これは、異なる型の変数の混在を禁じつつも、定数を柔軟に扱うために必要なのです。

sqrt2 := math.Sqrt(2)

const millisecond = time.Second/1e3
bigBufferWithHeader := make([]byte, 512+1e6)

おそらく、いままでの話を聞かずに、このコードを見せられたら、これがコンパイルできるのは当たり前に見えるかもしれません。しかし、それこそが、Go の定数が目指しているものなのです。つまり、


Because in Go, numeric constants work as you expect: like numbers.

By Rob Pike


ということなのです。


おまけ

ブログ記事中で、以下のような話が出ていて面白かったので、おまけで付け加えます。


uint 型の最大値を定数として宣言するにはどうしたら良いか?

パッと思い付く以下の式はだめです。というのも、uint が 32 ビットか 64 ビットかはアーキテクチャによって決まるからです。

const MaxUint32 = 1<<32 - 1

それでは、以下はどうでしょうか。

const MaxUint uint = -1 // Error: negative value

これは、C 言語では動きますが、Go 言語ではコンパイルエラーになります。

実は、ランタイム時なら uint-1 を代入できます。したがって、以下のコードは期待通りに動きます。

var u uint

var v = -1
u = uint(v)

しかし、これでは u が定数になりません。この後、どのように uint の最大値を定数として宣言するかは、ブログ記事を読んでみてください。