定食屋おろポン

おろしポン酢と青ネギはかけ放題です

中間値のroundによる丸め 〜Haskellのround関数を見て吹き出した件〜

C Way

round(1.5)は、2になります。 round(2.5)は、3になります。

それが、C言語流。俗にいう「四捨五入」の処理です。

ISO/IEC 9899 p233にも記載されています。

The round functions round their argument to the nearest integer value in floating-point format, rounding halfway cases away from zero, regardless of the current rounding direction.

すなわち

round関数は、引数のもっとも近い整数へと丸めます。 2つの整数の中間の値だった場合はゼロから遠ざかるように丸めます。

roundイコール四捨五入。そう、世の中はCで出来ている。

Haskell Way

Haskellでのroundはどうでしょうか。

Haskellのドキュメントを見て驚きました。

round x returns the nearest integer to x; the even integer if x is equidistant between two integers

すなわち

round xは、xにもっとも近い整数値を返します。2つの整数の中間の値だった場合は偶数を返します。

「偶数を返します。」

_人人人人人人人人人_
> 偶数を返します <
 ̄Y^Y^Y^Y^Y^Y^Y^Y ̄

round 2.5は2になり、round 3.5は4になります。

何故そうした!!!!

僕が無知でした

すこし調べた結果、単に僕が無知だっただけだということがわかりました。

roundには「rounding mode」というモードがあり、言語によってはモードを指定できます。 もっとも近い整数値を返すrounding modeの中で代表的なのは下記の4種類のようです。

  • HALF_UP
    • 中間値: Away from zero
  • HALF_DOWN
    • 中間値: Toward zero
  • HALF_EVEN
    • 中間値: Even integer
  • HALF_ODD
    • 中間値: Odd integer

CやRuby等ではHALF_UPを、HaskellではHALF_EVENを採用しているに過ぎないということですね。 また、PythonもHALF_EVENです。*1

「中間値では偶数に丸める」と書くと少し奇妙に思えますが、「中間値ではIntの最下位ビットを0にするポリシーである」と考えればそこまで奇妙でもありません。

負値の丸め

マイナスの値に関しても、中間値をroundしたときの挙動は言語によって違います。

C

Cでは負値であっても「ゼロから遠ざかるように」丸めます。1.5を丸めると2になりますし、-1.5を丸めると-2になります。

Java

一方、Javaでは「1/2を加えて切り捨て」のために正値ではゼロから遠ざかりますが、負値ではゼロに近づきます。1.5を丸めると2ですが、-1.5を丸めると-1になります。

負値に対するceilfloorが言語によって挙動が違うのは有名な話ですが、中間値のroundも挙動が違うわけですね。

そもそも

1.5にせよ-2.5にせよ、有理数ライブラリを使うのでもなければ浮動小数点数なので近似値でしかない。 中間値をroundしたときの挙動が問題になる場合はコードに問題があることがほとんどでは、という感想もある。

とはいえ

Haskellでも四捨五入を行ないたい!!!

Preludeでのroundの実装を少し変えて、C言語スタイルのroundを書きました。

roundUpOn5 :: (RealFrac a, Integral b) => a -> b
roundUpOn5 x =  let (n,r) = properFraction x
                    m     = if r < 0 then n - 1 else n + 1
                in case signum (abs r - 0.5) of
                  -1 -> n
                  0  -> m
                  1  -> m
                  _  -> error "round default defn: Bad value"
*Main> roundUpOn5 0
0
*Main> roundUpOn5 (2.7)
3
*Main> roundUpOn5 (-2.7)
-3
*Main> roundUpOn5 (-1.5)
-2
*Main> roundUpOn5 (-2.5)
-3
*Main> roundUpOn5 (2.5)
3

追記:より簡潔な実装をコメントでご指摘いただきました。ありがとうございます!

roundUpOn5 :: (RealFrac a, Integral b) => a -> b
roundUpOn5 x
  | n <= -0.5 = m - 1
  | n >= 0.5 = m + 1
  | otherwise = m
  where (m, n) = properFraction x