I struggled for hours to get something like the following right:
to_xyz_regular :: (RealFrac t) => (RegularSpd t) -> (t, t, t)
to_xyz_regular spd@(RegularSpd lambda_min lambda_max spectrum delta inverse_delta) =
let
l = length cie_x - 1
il = 1.0 / fromIntegral l
samp i = sample spd $ lambda_min + inverse_delta * fromIntegral i
integrate curve = (foldr (+) 0 $
map (\i -> curve!!i * samp i) [0..l]) * il
in (integrate cie_x, integrate cie_y, integrate cie_z)
(this is a conversion routine from color SPDs / spectra to XYZ colors).
The values cie_XXX were defined as follows:
cie_start = 360.0
cie_end = 830.0
cie_x = [0.0001299000, 0.0001458470, 0.0001638021, 0.0001840037,
....]
cie_y = ....
cie_z = ....
but then giving me roughly
Could not deduce (t ~ Double)
from the context (RealFrac t)
....
the problem being that the cie_XXX values where stored as Double, where I really would have liked them being poylmorphic.
The solution I finally found was to add type signatures for those values:
cie_start :: (RealFrac t) => t
cie_end :: (RealFrac t) => t
cie_x :: (RealFrac t) => [t]
cie_y :: (RealFrac t) => [t]
cie_z :: (RealFrac t) => [t]
Now my question is: Is this the proper way to have polymorphic values/literals in Haskell?
It was a bit tough for me to find a solution on websearch, because books, tutorials etc. mention type signatures only for parametric functions -> Are values just parameterless functions?
The way you have chosen is the proper way to have polymorphic types for numeric literals in Haskell.
The reason Double was selected for the type of cie_XXX is due to Haskell's defaulting mechanism (which is described in greater detail here). Defaulting is invoked when an otherwise polymorphic type (contains a forall, either implicitly or explicitly) is required to be monomorphic by the monomorphism restriction or the type is ambiguous (contains type variables that appears only to the left of =>, e.g. a in (Read a, Show a) => String), and the type is constrained to be an instance of a Prelude or standard library defined subclass of Num. Defaulting forces the type to be unified with each entry in default (...) (starting with the left-most entry) until unification succeeds. The default default (...) is
default (Integer, Double)
If for some reason Float is the desired default for real numbers,
default (Integer, Float)
could be defined. If Float is the desired default for all numbers,
default (Float)
could be defined.
In the case of the original scenario,
cie_start :: Fractional a => a
is the most general type for cie_start (as well as cie_end). This type, when inferred, would violate the monomorphism restriction. However, in the presence of defaulting, a few types are first tried in the place of the type variable a. Given that the default default (...) is default (Integer, Double), Integer and Double are tried, in that order.
cie_start :: Fractional Integer => Integer
results in a constraint violation (Integer is not an instance of Fractional).
cie_start :: Fractional Double => Double
succeeds, and is simplified to
cie_start :: Double
To make the original scenario an error instead of defaulting to Double (and also disable defaulting entirely), add
default ()
at the top level of the module where cie_XXX are defined.
To make the scenario not require an explicit type signature and instead infer the most general type, add
{-# LANGUAGE NoMonomorphismRestriction #-}
to the top of the module where cie_XXX are defined (see Monomorphism restriction for more details about why this most general type is not used by default).
Overall, the method you have chosen to resolve this issue (adding an explicit type signature) is the best solution. It is considered a best practice to have type signatures on all top level definitions.
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