IEEE floating point signalling NaN (sNaN) in Haskell

▼魔方 西西 提交于 2020-01-24 03:02:09

问题


Is there any way to define signaling NaN in Haskell? I found two approaches to deal with NaNs:

1) use 0/0, which produces quite nan

2) package Data.Number.Transfinite, which has no signaling NaNs too.

PS Is there any way to put Word64 bit by bit into Double without writing C library?


回答1:


I have found one non-portable way:

{-# LANGUAGE ForeignFunctionInterface #-}
import Data.Word (Word64, Word32)
import Unsafe.Coerce
import Foreign
import Foreign.C.Types
foreign import ccall "fenv.h feenableexcept" -- GNU extension
    enableexcept :: CInt -> IO ()

class HasNAN a where
    signalingNaN :: a
    quietNaN :: a

instance HasNAN Double where
    signalingNaN = unsafeCoerce (0x7ff4000000000000::Word64)
    quietNaN = unsafeCoerce (0x7ff8000000000000::Word64)

instance HasNAN Float where
    signalingNaN = unsafeCoerce (0x7fa00000::Word32)
    quietNaN = unsafeCoerce (0x7fc00000::Word32)

main = do
    enableexcept 1 -- FE_INVALID in my system
    print $ show $ 1 + (quietNaN :: Float) -- works
    print $ show $ 1 + (signalingNaN :: Float) -- fails

which perfectly fails. It turned out that FPU exceptions are a bad idea for Haskell. They are disabled by default for a good reason. They are OK if you debug C/C++/something else in gdb. I don't want to debug Haskell core dumps due to its non-imperative nature. Enabling FE_INVALID exceptions causes 0/0 and add to NaNs in Data.Number.Transfinite and GHC.Real to crash. But 0/0 calculated before enableexcept doesn't produce exceptions in addition.

I will use some simple errors check in my task. I need sNaN in just one place.




回答2:


What about using Data.Maybe?

You would use Maybe Float as datatype (assuming you want to use Float), and Just x for the non-NaN value x, whereas Nothing would represent NaN.

However, you'd need to radd at least a Num instance to be able to calculate using Maybe Float instead of Float. You can use fromJust as an utility function for this.

Whether this is expressed as qNaN or sNaN entirely depends on your implementation.




回答3:


You could use custom operators instead of custom types like this (this avoids replacing any Float in your code`)

snanPlus :: Float -> Float -> Float
snanPlus a b = if isNaN(a) then error "snan"
                           else if isNaN(b)
                                then error "snan"
                                else a + b

-- Some testing code
main = do
    print $ 3.0 `snanPlus` 5.0 -- 8.0
    print $ (0/0) `snanPlus` 5.0 -- error

The second print triggers the error.

Note: I'm not sure if there is a better way of formatting this, and you should probably not use concrete types in the function signature.




回答4:


You can use Data.Ratio to produce Nan/Infinity by using the ratios 1/0 (infinity) or 0/0 (NaN).

A faster but less portable approach is to use GHC.Real which exports infinity and notANumber.

infinity, notANumber :: Rational
infinity   = 1 :% 0
notANumber = 0 :% 0

Usage:

Prelude Data.Ratio GHC.Real> fromRational notANumber :: Float
NaN

For checking NaN/infinity, Prelude has two functions isNaN and isInfinite.




回答5:


You can do something like this:

newtype SNaN a = SNaN { unSNaN :: a}


liftSNaN :: RealFloat a => (a -> a) -> (SNaN a -> SNaN a)
liftSNaN f (SNaN x)
  | isNaN x = error "NaN"
  | otherwise = SNaN . f $ x

liftSNaN' :: RealFloat a => (a -> b) -> (SNaN a -> b)
liftSNaN' f (SNaN x)
  | isNaN x = error "NaN"
  | otherwise = f $ x

liftSNaN2 :: RealFloat a => (a -> a -> a) -> (SNaN a -> SNaN a -> SNaN a)
liftSNaN2 f (SNaN x) (SNaN y)
  | isNaN x || isNaN y = error "NaN"
  | otherwise = SNaN $ f x y

liftSNaN2' :: RealFloat a => (a -> a -> b) -> (SNaN a -> SNaN a -> b)
liftSNaN2' f (SNaN x) (SNaN y)
  | isNaN x || isNaN y = error "NaN"
  | otherwise = f x y


instance RealFloat a => Eq (SNaN a)
  where (==) = liftSNaN2' (==)
        (/=) = liftSNaN2' (/=)


instance RealFloat a => Ord (SNaN a)
  where compare = liftSNaN2' compare
        (<) = liftSNaN2' (<)
        (>=) = liftSNaN2' (>=)
        (>) = liftSNaN2' (>)
        (<=) = liftSNaN2' (<=)
        max = liftSNaN2 max
        min = liftSNaN2 min


instance (Show a, RealFloat a) => Show (SNaN a)
  where show = liftSNaN' show


instance RealFloat a => Num (SNaN a)
  where (+) = liftSNaN2 (+)
        (*) = liftSNaN2 (*)
        (-) = liftSNaN2 (-)
        negate = liftSNaN negate
        abs = liftSNaN abs
        signum = liftSNaN signum
        fromInteger = SNaN . fromInteger


instance RealFloat a => Fractional (SNaN a)
  where (/) = liftSNaN2 (/)
        recip = liftSNaN recip
        fromRational = SNaN . fromRational

You'd need more type classes to get the full Float experience of course, but as you can see it's pretty easy boilerplate once the liftSNaN* functions are defined. Given that, the SNaN constructor turns a value in any RealFloat type into one that will explode if it's a NaN and you use it in any operation (some of these you might arguably want to work on NaNs, maybe == and/or show; you can vary to taste). unSNaN turns any SNaN back into a quiet NaN type.

It's still not directly using the Float (or Double, or whatever) type, but if you just change your type signatures pretty much everything will just work; the Num and Show instances I've given mean that numeric literals will just as easily be accepted as SNaN Float as they will be Float, and they show the same too. If you get sick of typing SNaN in the type signatures you could easily type Float' = SNaN Float, or even:

import Prelude hiding (Float)
import qualified Prelude as P

type Float = SNaN P.Float

Though I'd bet that would cause confusion for someone eventually! But with that the exact same source code should compile and work, provided you've filled in all the type classes you need and you're not calling any other code you can't modify that hard-codes particular concrete types (rather than accepting any type in an appropriate type class).

This is basically an elaboration on Uli Köhler's first suggestion of providing a Num instance for Maybe Float. I've just used NaNs directly to represent NaNs, rather than Nothing, and used isNan to detect them instead of case analysis on the Maybe (or isJust).

The advantages of using a newtype wrapper over Maybe are:

  1. You avoid introducing another "invalid" value to the doing (Just NaN vs Nothing), that you have to worry about when converting to/from regular floats.
  2. Newtypes are unboxed in GHC; an SNaN Float is represented at runtime identically to the corresponding Float. So there's no additional space overhaead for the Just cell, and converting back and forth between SNaN Float and Float are free operations. SNaN is just a tag that determines whether you would like implicit "if NaN then explode" checks inserted into your operations.


来源:https://stackoverflow.com/questions/21344139/ieee-floating-point-signalling-nan-snan-in-haskell

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!