Understanding type variables that only appear in the return type

156 Views Asked by At

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 and Floating 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.

2

There are 2 best solutions below

2
On BEST ANSWER

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)]]
0
On

To answer your last question:

How can I even know what the full concrete type returned from renderToList is?

Since typevariables appearing in type declarations in Haskell mean that the function must have the requierd type for all types you decide to use for the type variable, you can choose which concret type the type variable should have as long as all of the constraints (here Ord a and Floating a) are satisfied.