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)
...
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
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
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With