Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are floatRange, floatRadix and floatDigits functions?

According to Hackage, these functions of the RealFloat class are

... constant function[s] ...

If they always remain at the same value, no matter the argument, as suggested by this description, why not simply use:

class (RealFrac a, Floating a) => RealFloat a where
    floatRadix :: Integer
    floatDigits :: Int
    floatRange :: (Int, Int)
    ...
like image 564
schuelermine Avatar asked Oct 15 '18 07:10

schuelermine


3 Answers

Your proposed non-function methods would have type

floatRadix' :: RealFloat a => Integer
floatDigits' :: RealFloat a => Int
...

Those are ambiguous types: there is an a type variable, but it doesn't actually appear to the right of the => and thus can't be inferred from the context. Which is, in standard Haskell, really the only way you can infer such a type variable: local type signatures also can only to the signature head, not the constraint. so whether you write (floatDigits' :: Int) or (floatDigits' :: RealFloat Double => Int), it won't actually work – the compiler can't infer that you mean the instance RealFloat Double version of the method.

class RealFloat' a where
  floatDigits' :: Int
instance RealFloat' Double where
  floatDigits' = floatDigits (0 :: Double)
*Main> floatDigits' :: Int

<interactive>:3:1: error:
    • No instance for (RealFloat' a0)
        arising from a use of ‘floatDigits'’
    • In the expression: floatDigits' :: Int
      In an equation for ‘it’: it = floatDigits' :: Int
*Main> floatDigits' :: RealFloat Double => Int

<interactive>:4:1: error:
    • Could not deduce (RealFloat' a0)
        arising from a use of ‘floatDigits'’
      from the context: RealFloat Double
        bound by an expression type signature:
                   RealFloat Double => Int
        at :4:17-39
      The type variable ‘a0’ is ambiguous
    • In the expression: floatDigits' :: RealFloat Double => Int
      In an equation for ‘it’:
          it = floatDigits' :: RealFloat Double => Int

For this reason, Haskell does not allow you to write methods with ambiguous type in the first place. Actually trying to compile the class as I wrote it above gives this error message:

    • Could not deduce (RealFloat' a0)
      from the context: RealFloat' a
        bound by the type signature for:
                   floatDigits' :: forall a. RealFloat' a => Int
        at /tmp/wtmpf-file3738.hs:2:3-21
      The type variable ‘a0’ is ambiguous
    • In the ambiguity check for ‘floatDigits'’
      To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
      When checking the class method:
        floatDigits' :: forall a. RealFloat' a => Int
      In the class declaration for ‘RealFloat'’

The highlighted line cites however a GHC extension that says “it's ok, I know what I'm doing”. So if you add {-# LANGUAGE AllowAmbiguousTypes #-} to the top of the file with the class RealFloat' in it, the compiler will accept that.

What's the point though, when the instance can't be resolved at the use site? Well, it can actually be resolved, but only using another pretty new GHC extension:

*Main> :set -XTypeApplications 
*Main> floatDigits' @Double
53
like image 114
leftaroundabout Avatar answered Nov 08 '22 16:11

leftaroundabout


The problem with this is that you would construct the functions for multiple instances, like:

instance RealFloat Float where
    -- ...
    floatRadix = 2
    floatDigits = 24
    floatRange = (-125, 128)

instance RealFloat Double where
    -- ...
    floatRadix = 2
    floatDigits = 53
    floatRange = (-1021, 1024)

But now it creates a problem when you query for example floatDigits: for what instance should we take? The one for Float, or the one for Double (or another type)? All of these are valid candidates.

By using an a parameter, we can make a disambiguation, for example:

Prelude> floatDigits (0 :: Float)
24
Prelude> floatDigits (0 :: Double)
53

but it holds that the value of the parameter does not matter, for example:

Prelude> floatDigits (undefined :: Float)
24
Prelude> floatDigits (undefined :: Double)
53
like image 26
Willem Van Onsem Avatar answered Nov 08 '22 15:11

Willem Van Onsem


The RealFloat class is very old. Back when it was designed, nobody had worked out really good ways to pass extra type information to a function. At that time it was common to take an argument of the relevant type and expect the user to pass undefined at that type. As leftaroundabout explained, GHC now has extensions that do this pretty nicely most of the time. But before TypeApplications, two other techniques were invented to do this job more cleanly.

To disambiguate without GHC extensions, you can use either proxy passing or newtype-based tagging. I believe both techniques were given their final forms by Edward Kmett with a last polymorphic spin by Shachaf Ben-Kiki (see Who invented proxy passing and when?). Proxy passing tends to give an easy-to-use API, while the newtype approach can be more efficient under certain circumstances. Here's the proxy-passing approach. This requires you to pass an argument of some type. Traditionally, the caller will use Data.Proxy.Proxy, which is defined

data Proxy a = Proxy

Here's how the class would look with proxy passing:

class (RealFrac a, Floating a) => RealFloat a where
    floatRadix :: proxy a -> Integer
    floatDigits :: proxy a -> Int
    ...

And here's how it would be used. Note that there's no need to pass in a value of the type you're talking about; you just pass the proxy constructor.

foo :: Int
foo = floatDigits (Proxy :: Proxy Double)

To avoid passing a runtime argument at all, you can use tagging. This is often done with the tagged package, but it's quite easy to roll your own too. You could even reuse Control.Applicative.Const, but that doesn't communicate the intention so well.

newtype Tagged t a = Tagged
  { unTagged :: a }

Here's how the class would look:

class (RealFrac a, Floating a) => RealFloat a where
    floatRadix :: Tagged a Integer
    floatDigits :: Tagged a Int
    ...

And here's how you'd use it:

foo :: Int
foo = unTagged (floatDigits :: Tagged Double Int)
like image 3
dfeuer Avatar answered Nov 08 '22 16:11

dfeuer