I'm having some trouble understanding how to understand and use type variables that only appear in the return type of a function.
I'm trying to use diagrams-cairo to compare two diagrams, pixel by pixel. The renderToList function has the type:
renderToList :: (Ord a, Floating a) => Int -> Int -> Diagram Cairo R2 -> IO [[AlphaColour a]]
Returning a list of lists of AlphaColour a
. Bearing in mind that a
is (Ord a, Floating a)
, I figured I could use mathematical and comparison operations on these AlphaColour a
values:
import Diagrams.Prelude
import Diagrams.Backend.Cairo
import Diagrams.Backend.Cairo.List
import Data.Colour
import Data.Colour.SRGB
import Data.Foldable (fold)
import Data.Monoid
cmp :: Diagram Cairo R2 -> Diagram Cairo R2 -> Diagram Cairo R2 -> IO Bool
cmp base img1 img2 = do
baseAlphaColours <- renderToList 400 400 base
img1AlphaColours <- renderToList 400 400 img1
img2AlphaColours <- renderToList 400 400 img2
return $ (imgDiff baseAlphaColours img1AlphaColours) < (imgDiff baseAlphaColours img2AlphaColours)
imgDiff :: (Ord a, Monoid a, Floating a) => [[AlphaColour a]] -> [[AlphaColour a]] -> a
imgDiff img1 img2 = fold $ zipWith diffPix (concat img1) (concat img2)
diffPix :: (Ord a, Floating a) => AlphaColour a -> AlphaColour a -> a
diffPix a1 a2 = (diffRed * diffRed) - (diffGreen * diffGreen) - (diffBlue * diffBlue)
where red a = channelRed $ toSRGB (a `over` black)
green a = channelGreen $ toSRGB (a `over` black)
blue a = channelBlue $ toSRGB (a `over` black)
diffRed = (red a1) - (red a2)
diffGreen = (green a1) - (green a2)
diffBlue = (blue a1) - (blue a2)
However I'm getting the ominous compile error
Ambiguous type variable `a0' in the constraints:
(Floating a0)
arising from a use of `renderToList' at newcompare.hs:11:37-48
(Ord a0)
arising from a use of `renderToList' at newcompare.hs:11:37-48
(Monoid a0)
arising from a use of `imgDiff' at newcompare.hs:14:27-33
Probable fix: add a type signature that fixes these type variable(s)
In a stmt of a 'do' block:
baseAlphaColours <- renderToList 400 400 base
In the expression:
do { baseAlphaColours <- renderToList 400 400 base;
img1AlphaColours <- renderToList 400 400 img1;
img2AlphaColours <- renderToList 400 400 img2;
return
$ (imgDiff baseAlphaColours img1AlphaColours)
< (imgDiff baseAlphaColours img2AlphaColours) }
In an equation for `cmp':
cmp base img1 img2
= do { baseAlphaColours <- renderToList 400 400 base;
img1AlphaColours <- renderToList 400 400 img1;
img2AlphaColours <- renderToList 400 400 img2;
.... }
Which I understand as the compiler wanting to know the full type of the renderToList
calls.
But what I don't understand is:
Ord
and Floating
instances.renderToList
is?I feel I'm missing something fundamental with the way this code is written, any help would be greatly appreciated.
Type variables that only appear in a return type are generally fine, because the Hindley-Milner algorithm that's at the core of Haskell's type inference is two-way: both the way an value is generated and the way that it is used go into determining what concrete type it should have.
Often the right value for a type variable in the return type will be determined by context, for example if you have
data Foo = Foo Int
and then you write
mkFoo :: String -> Foo
mkFoo x = Foo (read x)
then despite read
having type Read a => String -> a
, there'll be no problem, because it'll be clear to the compiler that the return type of read
needs to be Int
in this context.
However here your type variable is fundamentally ambiguous: you are generating it with renderToList
, doing something more to it with imgDiff
, and then finally "consuming" it with <
which has type a -> a -> Bool
- the way the result of <
is used can't help determine what a
should be.
So there's no context anywhere for the compiler to work out what type should actually be used. Even though only operations from Floating
and Ord
are needed, those operations have concrete implementations on each type, and the values of each type also have their own concrete representations. So the compiler really has to choose one type.
You can fix this quite simply by just adding a type signature. In this case adding one to the line that sets up baseAlphaColours
should do it, because all the other uses are constrained by the signatures of the other functions:
For example to choose Float
, you could change the relevant line to:
baseAlphaColours <- renderToList 400 400 base :: IO [[AlphaColour Float]]
In this case the requirements are actually slightly more complicated than just Floating
and Ord
. So Float
may not work as it doesn't normally have a Monoid
instance. If you get an error about "no instance for Monoid Float", you may need to use a different type.
If you expect the images to be composed by point-wise addition of individual pixels, then the right type to use would be something like Sum Float
, where Sum
is obtained from Data.Monoid
. So something like:
baseAlphaColours <- renderToList 400 400 base :: IO [[AlphaColour (Sum Float)]]