すごい乱暴な方法で Haskell の小数を正確数にする

  • 0
    いいね
  • 1
    コメント
    この記事は最終更新日から1年以上が経過しています。

    そもそも Haskell の Prelude で toRational :: Real a => a -> Rational が宣言されているので,これを使えば小数を有理数として扱えはするけれど,以下のように望ましくない動きをする

    import Data.Ratio ((%))
    11 % 10  -- Haskell での 11/10 (= 1.1) の有理数表現
    fromRational $ 11 % 10 -- 1.1 これは期待通り
    fromRational $ toRational 1.1 -- 1.1 これも期待通り
    (toRational 1.1) == (toRational 1.1) -- True これも期待通り
    toRational 1.1 -- 2476979795053773 % 2251799813685248 えっ
    (toRational 1.1) == (11 % 10) -- False そんな…
    

    fromRationaltoRational を連続して適用する場合は割と望ましい結果が得られるが,toRational のみを利用して,他の有理数との算術演算をしたりするとどうもおかしい事になりがち.
    なので多少乱暴な方法で正確数を Real a ( 実数型クラスのインスタンス ) から得る関数を書く

    import Numeric ( floatToDigit, fromRat )
    import Data.Ratio ( (%), denominator )
    
    -- | exact ratio number from Real `a`
    -- example
    -- > exact 1.1 -- 11 % 10
    -- > (exact 1.1) == (11 % 10) -- True
    -- > fromRational $ exact 1.1 -- 1.1
    exactFromReal :: Real a => a -> Rational
    exactFromReal x
      | asInt      = ratio
      | otherwise  = numer % denom
      where
      asInt  = denominator ratio == 1
      ratio  = toRational x
      numer  = round $ ratio * (denom % 1)
      denom  = 10 ^ (uN - iN)
      iN     = snd digits
      uN     = length $ fst digits
      digits = floatToDigits 10 $ fromRat ratio
    

    ただし,ここで利用している Numeric.floatToDigits が,十進で17桁以上は切ってしまうので, 精度はそこまで良いわけではない.
    QuickCheck でテストした場合.だいたい ±10^(-12) (ピコ) の精度は保証できている