これは何?
go の型推論が思いがけない動作をしたのでその記録。
普通の例
func main() {
f := func(i int) uint8 {
return uint8((1 << i) % 10)
}
fmt.Println("f(5) =", f(5)) //=> f(5) = 2
}
$2^n$ を 10で割った余りを uint8
で返すという普通の関数。
2の5乗 は 32 なので、 f(5)
は 2 になる。順当。
思いがけなかった例
上記の関数 f
に 10 を入れると
fmt.Println("f(10) =", f(10)) //=> f(10) = 0
$2^{10}$ は 1024 なので f(10) = 4
となるかと思いきや、ゼロになる。
私は意外だった。
いろいろ試した
一回変数で受ける
f := func(i int) uint8 {
n := 1 << i
return uint8(n % 10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 4
一回受けた変数 n
は int
になるらしく、ちゃんと 4 を返す。
1
ではなく one
にする
f := func(i int) uint8 {
const one = 1
return uint8((one << i) % 10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 0
one
にしても untyped int のままなので、やはり即値を書くのとおなじになる。
const one int = 1
f := func(i int) uint8 {
const one int = 1
return uint8((one << i) % 10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 4
one
が int
なら、オーバーフローしない。
むだに int(0)
を足す
f := func(i int) uint8 {
return uint8(int(0) + (1<<i)%10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 4
int(0)
を足すと 1<<i
の 1
が int
だと推論される。
1
ではなく '\x01'
にする
f := func(i int) uint8 {
return uint8(('\x01' << i) % 10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 0
untyped rune でも untyped int と同様。
式の途中に負を入れる
負の値なら uint8
だと推論されないだろうと思うと
f := func(i int) uint8 {
return uint8(-1 + 1 + (1<<i)%10)
}
fmt.Println("f(10) =", f(10)) //=> f(10) = 0
そうでもない。
ならばと順序を変えると
f := func(i int) uint8 {
return uint8(-1 + (1<<i)%10 + 1) //=> -1 (untyped int constant) overflows uint8
}
fmt.Println("f(10) =", f(10))
オーバーフローでエラーになる。
関数に渡す場合も同様
func main() {
u8 := func(x uint8) { fmt.Println("x =", x) }
u32 := func(x uint32) { fmt.Println("x =", x) }
i := 10
u8((1 << i) % 10) //=> x = 0
u32((1 << i) % 10) //=> x = 4
const ci = 10
u8((1 << ci) % 10) //=> x = 4
u32((1 << ci) % 10) //=> x = 4
}
関数に渡す場合でも同様に、引数の型によって untyped int の型が推論され、計算結果に影響を与える。
ただし、 シフトする前に型を推論するのは untyped ではない値に遭遇した場合のみ。
上記コードの i
は untyped ではないので 1 << i
にある 1 の型を推論する必要が生じる(しかし、推論に i
の型は使わない)。
上記コードの ci
は untyped なので、 1 << ci
にある 1 の型を推論する必要がなく、untyped のまま (1 << ci) % 10
を計算できる。
まとめ
というわけで go 言語は
uint8((1 << i) % 10)
のような式がある場合、uint8
の部分に書かれる型によって 1
の型が推論され、推論の結果オーバーフローになったりならなかったりするという仕様っぽい。
私は
(1<<i) % 10
の部分は uint8
の部分とは無関係に計算され、その結果を uint8
にキャストするんだと思っていたんだけど、そうではない。
また。
(式A) + (式B)
のような式があると、式A の型は 式B の中にある untyped な型の推論に影響を与えるので 式A の型が 式B の計算結果に影響を与える。
関数呼び出しでも同様で 関数(式X)
のように書く場合、関数の引数の型が 式X の中にある untyped な型の推論に影響を与えるので 関数の引数の型が 式X の計算結果に影響を与える。
大変わかりにくく、トラブルの原因になりやすい仕様だと思うけど、変わらないと思う。気をつけよう。
おまけ: zig の場合
ビット長不定の定数があるもう一つの言語ということで zig の場合を見てみる。
zig で似たようなことを試みると、
fn f(a: i32) u8 {
return @intCast(u8, (1 <<| a) % 10);
// ↑ error: LHS of shift must be a fixed-width integer type, or RHS must be comptime-known
}
zig は、ビット長不定の値をシフトできないことがわかる。(エラーメッセージにある通りシフト量がコンパイル時にわかっているのであればOK)
いろいろ考えたんだけど、今の所 go で起きているようなわかりにくい状況を引き起こす方法は思いついていない。
よくできていると思う。