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:
- Why does the compiler need to know the full type? I think I'm only using operations available to
Ord
andFloating
instances. - If I do need to provide a type, where exactly in the code would I define this type.
- How can I even know what the full concrete type returned from
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
and then you write
then despite
read
having typeRead a => String -> a
, there'll be no problem, because it'll be clear to the compiler that the return type ofread
needs to beInt
in this context.However here your type variable is fundamentally ambiguous: you are generating it with
renderToList
, doing something more to it withimgDiff
, and then finally "consuming" it with<
which has typea -> a -> Bool
- the way the result of<
is used can't help determine whata
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
andOrd
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:In this case the requirements are actually slightly more complicated than just
Floating
andOrd
. SoFloat
may not work as it doesn't normally have aMonoid
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
, whereSum
is obtained fromData.Monoid
. So something like: