ネギのメモ帳

Twitterに書ききれないことをたまに書いたりするかもしれないスペース

昨日の記事のdigit(桁数)関数のバグを修正

ガードにおいてはotherwiseのような自明な全捕捉でない限り
コンパイラーは条件が網羅的かどうか推論できないらしいということを学んだ.


それはそれとして, 昨日書いたdigitにはバグがあることに気づいた*1.
符号付き整数に関して,

let m = (minBound :: Int) in m == (-1) * m --> True

が成り立つので, digit (minBound :: Int) は無限ループに陥ってしまう.


回避の仕方はいくつかあると思うけど,
(minBound :: a) が-10^n の格好で与えられるような型aが存在する場合にも
対応しようと思うと, divを用いて求める方法が安全と思われる.
すなわち, 10で割った数に対して digitを求め, それに1を加えて答えとする.

digit :: (Integral a) => a -> a
digit n
  | n >  0        = (+1) . floor . (logBase 10) $ (fromIntegral n :: Float)
  | n == 0        = 1
  | n == negate n = (+1) . digit . abs $ n `div` 10
  | otherwise     = digit $ abs n

通常は符号付き整数の下限は -2^n (n>0)の形で与えられるはずだから,
その10進表記の末尾は2,4,6,8のいずれかになるので,
1を足しても繰り下がりはせず,
digit (-2^n) == digit (-2^n +1)
が成り立つはずである. よって以下の定義でも普通は問題ないような気がする.

  | n == negate n = digit . abs $ n + 1


あとガードの順序は割と重要.
negate 0 == 0 だし, あと符号なし整数に対しては例えば

import Data.Word (Word8)
negate (128 :: Word8) == 128 --> True

が成立してしまうので, この条件判定は非負の整数に適用されるとまずい.


あるいはおとなしく文字列の長さを数えるか…

import Data.List (genericLength)
digit :: (Show a, Integral a) => a -> a
digit = genericLength . (dropWhile (=='-')) . show

*1:DateCreateで使う分には関係ない部分の話ではある.