Opened 8 years ago

Last modified 20 months ago

#3676 new bug

realToFrac doesn't sanely convert between floating types

Reported by: draconx Owned by:
Priority: normal Milestone:
Component: Core Libraries Version: 6.12.1
Keywords: report-impact Cc: daniel.is.fischer@…, me@…, core-libraries-committee@…, ekmett
Operating System: Unknown/Multiple Architecture: x86_64 (amd64)
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:

Description (last modified by igloo)

As far as I can tell, the only way to convert between floating types in Haskell is to use realToFrac. Unfortunately, this function does some insane mangling of values when converting. Some examples:

realToFrac (0/0 :: Double) :: Double
  --> -Infinity
realToFrac (-1/0 :: Float) :: Double
  --> -3.402823669209385e38
realToFrac (-0 :: Double) :: CDouble
  --> 0.0

The last item illustrates an important point: it is impossible to convert a value from Double to CDouble without potentially changing it. This makes it difficult or impossible to use the FFI to call any functions with floating point parameters/return values.

Using a combination of unsafeCoerce (to shoehorn Double values in/out of CDoubles) and some functions written in C (to perform float<=>double), I was able to work around these problems, but that hardly seems like a nice solution.

Change History (26)

comment:1 Changed 8 years ago by igloo

Description: modified (diff)

comment:2 Changed 8 years ago by igloo

Milestone: 6.12.2

Thanks for the report.

comment:3 Changed 8 years ago by igloo

See also #3070

comment:4 Changed 8 years ago by draconx

Version: 6.10.46.12.1

Still present in 6.12.1.

I just discovered that the issue is worse than originally described: the behaviour of realToFrac *changes* if it gets inlined. Consider the following:

module Main where

{-# NOINLINE holycow #-}
holycow :: (Real a, Fractional b) => a -> b
holycow = realToFrac

main = do
    print (realToFrac (0/0 :: Double) :: Double)
    print (holycow (0/0 :: Double) :: Double)

If you compile without optimisation, both lines print -Infinity as originally described. However, if you enable optimisation, the first line prints NaN (yay!) but the second line still prints -Infinity. GHC seems to be optimising based on the incorrect assumption that realToFrac :: Double -> Double is the identity function.

comment:5 Changed 8 years ago by simonpj

Yes, that's because the library GHC.Float contains the (clearly incorrect) RULE

"realToFrac/Double->Double" realToFrac   = id :: Double -> Double

This ticket really really needs someone who knows and cares about numbers to figure out what the Right Thing is. It's a library issue involving only Haskell source code. Would someone like to help?

Thanks

Simon

comment:6 Changed 8 years ago by isaacdupree

my gut feeling has been there should be a function or functions specifically to convert between different floating types. I think this is something that needs to be invented and proposed on libraries@ . (I'm not volunteering to do it this week because I'm going to a conference, but I'm willing to get the conversation started next week)

-Isaac Dupree

comment:7 Changed 8 years ago by simonmar

Owner: set to simonmar
Status: newassigned

comment:8 Changed 8 years ago by simonmar

I think it's pretty straightforward. toRational should be able to convert NaN, Infinity and -Infinity correctly to Rational, and there is already code in fromRational to convert them back to the floating point types correctly (I imagine it's there in order to make things like read "NaN" work).

The Haskell report doesn't say anything about the implementation of toRational, so I think this is correct. I'm validating a patch now.

comment:9 Changed 8 years ago by draconx

Don't forget negative zero.

comment:10 Changed 8 years ago by simonmar

I've fixed this, but I'm not sure the fix is an improvement.

Thu Feb 11 02:19:55 PST 2010  Simon Marlow <marlowsd@gmail.com>
  * Handle NaN, -Infinity and Infinity in the toRational for Float/Double (#3676)

    M ./GHC/Float.lhs -2 +9
    M ./GHC/Real.lhs -1 +2

and in ghc:

Thu Feb 11 05:15:43 PST 2010  Simon Marlow <marlowsd@gmail.com>
  * don't constant fold division that would result in negative zero (#3676)

It has some slightly odd consequences. Someone ought to take a broader look at this in the context of refining the Haskell standard. e.g.

Prelude> toRational (-0 :: Double) == - toRational (0 :: Double)
False

whereas previously this was True.

The underlying problem is that the Rational type doesn't really include these strange values; it has been hacked in a few places so that certain unnormalised Rational values are used to represent strange floating point values, e.g. 0 :% 0 represents NaN and fromRational knows about it, but there's no official way to generate one of these (0/0 :: Rational gives an exception), they are only used "internally" by the libraries to make read "NaN" :: Double work.

The change I made is to make toRational (0/0 :: Double) generate 0 :% 0, and similarly for the other strange values, but these had odd consequences, because these special Rational values are not normalised. However, this does fix a real bug (the subject of this ticket).

What do people think?

comment:11 Changed 8 years ago by draconx

While it presumably goes against the report, I personally feel that floating point types do not belong as members of the Real class *at all*. The same is true for a number of other classes that they belong to.

In the altfloat package, where I've been experimenting with solutions to this and other problems, I used a class

class FloatConvert a b where
    toFloating :: a -> b

which provides the necessary conversions: both between floating point types, and from real types to floating point types. It should be noted that the functions double2Float# and float2Double# in GHC.Exts perform correct conversion.

comment:12 Changed 8 years ago by maeder

I suppose arithmetic on these non-normalised rationals does also not behave like on doubles (IEEE). I would expect toRational to fail on NaN and Infinity and to convert -0 to 0.

But rather than introducing a multi-parameter type class I would just add non-overloaded plain conversion functions between Float, Double, CDouble, etc.

comment:13 Changed 8 years ago by draconx

The problem with non-overloaded functions is that there are a lot of them: 12 with the four floating types that currently exist, and 30 if long double is ever added (two types: LDouble & CLDouble). This problem could be mitigated by only providing the trivial conversions when FFI types are involved: that is, provide Double <=> CDouble, but don't bother with Float <=> CDouble.

On the other hand, we could use a single parameter version of altfloat's technique as follows:

class FloatConvert a where
    toDouble :: a -> Double

since Double can represent all values of every other floating type (at least until long double exists). Then we could have a fromDouble member in the RealFloat class, and define toFloating = fromDouble . toDouble

comment:14 Changed 8 years ago by simonmar

Milestone: 6.12.2_|_

I rolled back the patch to libraries/base:

Tue Feb 23 10:16:03 GMT 2010  Simon Marlow <marlowsd@gmail.com>
  * UNDO: Handle NaN, -Infinity and Infinity in the toRational for Float/Double (#3676)

in retrospect it wasn't a good idea to shoehorn these strange values into Rational.

The position now is that toRational (1/0 :: Double) is undefined. The Haskell standard does not define the result (it should), and GHC gives unpredictable results. Hence, realToFrac cannot convert accurately between floating-point types.

Someone should:

  • propose a proer API for conversion between floating-point types, e.g. FloatConvert above, but make a full proposal.
  • decide whether toRational (1/0::Double) should be undefined or an exception. If it is an exception, then we cannot optimise realToFrac to a direct conversion, e.g. floatToDouble#.

comment:15 Changed 8 years ago by draconx

I agree that the "funny rationals" are a bit too strange a notion, especially when it comes to defining how arithmetic operations should behave.

After thinking about it, I'm fond of the toDouble/fromDouble approach as it seems consistent with the themes elsewhere in the library, except that it makes it harder to later add floating types that are wider than Double.

I can post this idea to the haskell-prime mailing list for discussion, if that's the right one.

comment:16 Changed 8 years ago by draconx

OK, I've posted this to the haskell-prime mailing list.

http://thread.gmane.org/gmane.comp.lang.haskell.prime/3146

comment:17 Changed 7 years ago by simonmar

Owner: simonmar deleted

comment:18 Changed 6 years ago by daniel.is.fischer

Cc: daniel.is.fischer@… added

Summarising:

There is no good solution to the problem while the conversions go (potentially, without rewrite rules) via Rational. Having 'illegal' Rational values like 1 :% 0 generated by parsing Infinity and so on is not a big problem because those are immediately passed to fromRational, so their treatment is confined to one place and they can be handled meaningfully there. But if they could be generated by toRational, every operation on Rationals would have to check for these values (and it's not always clear how to handle them).

The options I'm aware of are

  1. non-overloaded conversion functions between each pair of types
  2. a two-parameter conversion class
  3. adding toMaxFloat and fromMaxFloat functions to RealFloat
  4. adding a dedicated pseudo-numeric type for conversions

All of them have downsides. 1. and 2. would require a number of functions resp. instances quadratic in the number of involved types - yuck. 1. would even rule out polymorphic code. 3. has the problem of what MaxFloat should be and any choice would cause breakage when a larger RealFloat type is introduced. 4: A type with the sole purpose of facilitating numeric conversions in the standard libraries is quite wartish (and new types could introduce new unrepresentable values).

I think 4. is the least undesirable option, but still it would be a lot of work to implement (it involves a change to the language report, among other things) for dubious benefit.

Unless renewed concern is expressed, I think we should mark it as low priority.

comment:19 Changed 4 years ago by carter

difficulty: Unknown

Hrm, so we need a good conversion story / semantics for mapping between different floating point types? This is near and dear to some of my current work, I'll look into coming up with a good story, and if successful, propose it to the libraries list.

comment:20 Changed 3 years ago by thoughtpolice

Component: libraries (other)Core Libraries
Owner: set to ekmett

Moving over to new owning component 'Core Libraries'.

comment:21 Changed 3 years ago by lelf

Cc: me@… core-libraries-committee@… added

comment:22 Changed 3 years ago by ekmett

Keywords: report-impact added

Any action we take on this would have an effect on the Haskell Report. Noting this accordingly.

comment:23 Changed 2 years ago by thomie

Owner: ekmett deleted

comment:24 Changed 20 months ago by Rufflewind

Cc: ekmett added

I think it would be a useful to at least note this problem in the docs for realToFrac:

-- | general coercion to fractional types
--
--   Warning: avoid using this function to convert between floating-point
--   number types. Consider using 'GHC.Float.float2Double' and
--   'GHC.Float.double2Float' instead.  See
--   <https://ghc.haskell.org/trac/ghc/ticket/3676 GHC bug #3676>.

(Incidentally, the documentation for GHC.Float is hidden from the users.)

comment:25 Changed 20 months ago by ekmett

I definitely agree that documenting the issues around realToFrac would be a good idea.

comment:26 Changed 20 months ago by simonmar

Documenting things is good, but

  • Instead of linking to the ticket, explain *why* the function shouldn't be used
  • We cannot recommend using internal APIs (GHC.Float)
Note: See TracTickets for help on using tickets.