How (not) to convert CDouble to Double
Published on
What’s wrong with the following code?
module Acos (acos) where
import Prelude hiding (acos)
import Foreign.C.Types (CDouble(..))
import ccall "math.h acos" c_acos :: CDouble -> CDouble
foreign
acos :: Double -> Double
acos = realToFrac . c_acos . realToFrac
If you use QuickCheck to test the equivalence of
Acos.acos
and Prelude.acos
, you’ll quickly
find a counterexample:
> Prelude.acos 1.1
NaN
> Acos.acos 1.1
Infinity
You might think this is a difference in the semantics of Haskell acos vs. C acos, but the acos(3) manpage disproves that:
If x is outside the range [-1, 1], a domain error occurs, and a NaN is returned.
Moreover, you’ll notice the discrepancy only when compiling the
Haskell program with -O0
. If you compile with
-O1
or higher, both versions will result in
NaN
. So what’s going on here?
What turns the NaN
turned into the Infinity
is realToFrac
. It is defined as follows:
realToFrac :: (Real a, Fractional b) => a -> b
realToFrac = fromRational . toRational
Unlike Double
, Rational
, which is defined
as a ratio of two Integers, has no way to represent special values such
as NaN
. Instead, toRational (acos 1.1)
results
in a fraction with some ridiculously large numerator, which turns into
Infinity
when converted back to Double
.
When you compile with -O1
or higher, the following
rewrite rules fire and avoid the round trip through
Rational
:
"realToFrac/a->CDouble" realToFrac = \x -> CDouble (realToFrac x)
"realToFrac/CDouble->a" realToFrac = \(CDouble x) -> realToFrac x
"realToFrac/Double->Double" realToFrac = id :: Double -> Double
Unfortunately, the Haskell
2010 Report doesn’t give you any reliable way to convert between
Double
and CDouble
. According to the Report,
CDouble
is an abstract newtype, about which all you know is
the list of instances, including Real
and
Fractional
. So if you want to stay portable,
realToFrac
seems to be the only solution available.
However, if you only care about GHC and its base library (which
pretty much everyone is using nowadays), then you can take advantage of
the fact that the constructor of the CDouble
newtype is exported.
You can use coerce
from Data.Coerce
or apply
the data constructor CDouble
directly.
So here’s a reliable, but not portable, version of the
Acos
module above:
module Acos (acos) where
import Prelude hiding (acos)
import Foreign.C.Types (CDouble(..))
import Data.Coerce (coerce)
import ccall "math.h acos" c_acos :: CDouble -> CDouble
foreign
acos :: Double -> Double
acos = coerce c_acos