Scribble at 2022-05-27 13:16:56 Last modified: 2022-05-27 14:49:30
引き続き Go の話をするのだが、さきほどから休憩に入って再びテキストを読み進めながらコードを書いて動かしているのだけれど、どうにも困ったことが一つある。それは、上記で引用した RoundToEven() という math モジュールの関数だ。簡単に言えば、ぜんぜん「最も近い偶数」を返してくれないのである。
fmt.Println( math.RoundToEven( 3.4 ) ) // results 3
fmt.Println( math.RoundToEven( 3.01 ) ) // results 3
fmt.Println( math.RoundToEven( 3.88 ) ) // results 4
fmt.Println( math.RoundToEven( 3.44 ) ) // results 3
このとおり、早い話が小数点以下を四捨五入しており、math.Round() とまるっきり同じ結果が出る。定義のとおりに理解すれば、上記の float64 の値はすべて(偶数の)2から(偶数の)4までのあいだで、どれもが(偶数の)4に近い数なのだから(3より大きければ常に2よりも4の方が近い)、すべて4が出力されなくてはならない筈だ。でも、そもそも検索しても RoundToEven() について書かれているウェブ・ページそのものが殆どないし、あってもマニュアルからコピペしたような説明ばかりで役に立たない。
もちろん、その事情は分からなくもない。PHP でも言えることだが、実は ceil() とか round() なんて関数は現実のプロがコーディングしているシステム開発の現場では殆ど使わないからだ。実際、この手の変換関数というのはプログラミング言語ごとにサポートされていたりいなかったりするし、挙動が違っていたり、場合によってはバグが放置されていたりする(使われていないからバグも発見されにくい)。なので、たいていのプロはこんな関数は危なくて使おうとしないし、大多数のケースで数値を丸める必然性など何もないし、おまけに財務や経理のデータであれば法律や税務計算の要件というものがある。
で、自分でもなかなか割り切れないのだが、こういう事例がいくつかあると「この言語は危なくて使えないな」と思って学習する意欲が減退してしまうのだ。要は、馬鹿が処理系をいじってると判断できてしまうからだ。(ブライアン・カーニハンに言ってるわけではないが。)
で、こういう場合に何をするべきかは明白だ。Go の math パッケージのソースコードを確認するのだ。オープン・ソースで開発されているメリットの一つは、疑問があれば自分で確認できる(逆に言えば、自分で確認しなくてはいけない)ことにある。これをしないで他人が開発しているものを無能だの馬鹿だのと書くのは、もちろん無礼なことだろう。そして、もともとテキストなどに書いてある雑な説明だけでは正確な挙動がわからないのだ。なぜなら、math.RoundToEven( 3.0 ) の場合に、2と4のどちらが「近い」のか、"nearest" しか条件がなければ決められないからだ。もしこの条件だけで、2が「近い」とか4が「近い」などと言う math パッケージの開発者がいるなら、もう小学生の算数からやりなおせという話になってしまう。そういうわけで、Go の math パッケージを見ると、Go のパッケージは Go で書かれているらしく、以下の URL を参照して最新版のソースを確認できる。
https://cs.opensource.google/go/go/+/master:src/math/floor.go
実際にコンパイルされるコードは shift とか bit などの他のソースで定義されている値を使うため、依存関係を調べるのが大変で floor.go に書かれている内容だけでは推測しかできないが、ソースに書かれている処理内容の説明を見ると、以下のようになっている。
func RoundToEven(x float64) float64 {
t := math.Trunc(x)
odd := math.Remainder(t, 2) != 0
if d := math.Abs(x - t); d > 0.5 || (d == 0.5 && odd) {
return t + math.Copysign(1, x)
}
return t
}
最初に、入力した値 x の小数点以下を math.Trunc() で切り捨てている。次に奇数(2で割った余りが0でない)かどうかを判定して odd にブール値として格納し、x の小数点以下が 0.5 よりも大きいか、または d が 0.5 であって切り捨てた t が奇数であれば、t に x の符号をもつ1を足した値を返すというわけである・・・おいおい。これじゃあ、小数点以下が 0.5 よりも大きいというだけで、切り捨てる前の整数部が奇数でも偶数でも、1を足して返してしまうことにならないか? なるほど、こんなソース・コードを実装しているなら、math.RoundToEven( 2.6 ) が 3 という奇数になってしまうのもしょうがない。そういうわけで、math.RoundToEven() はガラクタ関数に決定だ。