Haskell fails to infer the return type of a monad after using the sequence operator

74 Views Asked by At

The following code

import Control.Monad.Writer

class Foo c where
    fromInt :: Int -> c

instance Foo [Int] where
    fromInt n = [n]

instance (Monoid c, Foo c) =>  Foo (Writer c ()) where
    fromInt d = writer ((),fromInt d)

onetwo :: Writer [Int] ()
onetwo = fromInt 1 >> fromInt 2

yields the following error :

Ambiguous type variable `a0' arising from a use of `fromInt' prevents the constraint 
`(Foo (WriterT [Int] Data.Functor.Identity.Identity a0))' 
from being solved.

So it looks like Haskell fails to infer the return type of the Writer monad, but I don't understand why, since it can only be the unit type based on the implementation of fromInt.

Why does Haskell fail to infer this ?

2

There are 2 best solutions below

1
willeM_ Van Onsem On BEST ANSWER

The problem is essentially the left operand in fromInt 1 >> fromInt 2. Indeed, (>>) has as type (>>) :: Monad m => m a -> m b -> m b. This means that with the type of the result, we know what m and b are, but not a.

That is a problem here. Yes, we defined an instance instance Foo (Writer c ()), and currently that is the only instance. But that does not exclude that (another) programmer could later add an instance:

instance Foo (Writer [Int] Bool)

-- …

for example, and since a is not known, it can be anything.

We can fix this by specifying the type of the first operand, for example with:

onetwo :: Writer [Int] ()
onetwo = (fromInt 1 :: Writer [Int] ()) >> fromInt 2
0
Ben On

Take a look at this instance head:

instance ... =>  Foo (Writer c ())

This says that GHC can use this instance whenever it needs to solve a Foo constraint for a type that looks like Writer c () (for some c). And once it has decided to use this instance it will then need to solve the constraints in the ... part, but they do not influence whether this instance is chosen.

But note carefully that GHC has to already know that the second parameter of Writer is () in order to decide that this instance applies. Thus there is no way that GHC can ever use this instance to help infer that some type is (); we can only use the instance if we had already inferred the type! If it couldn't infer that from other information then you get an ambiguous constraint error, as GHC doesn't know whether it is allowed to use this instance or not.

This is the heart of why the type equality constraint trick (suggested by chi and Iceland_Jack) works. It changes the instance head to look like this:

instance ... =>  Foo (Writer c a)

Now a and c are simple independent type variables; Writer applied to any two things will match and allow GHC to use the instance. Once it does it will look at the ... constraints and see a ~ (). At that point it will still raise a type error if a is already established to be something else, but if a is unconstrained it could be () and so that gets added to the information GHC knows about that type.

Effectively it lets us infer extra type information when we use the instance, instead of requiring inferred type information in order to use the instance.

The other side of the coin is that GHC really will try to use this instance regardless of what the type a is. So you can't support any other instances where it is something other than (), or you'll end up with multiple conflicting instances matching. In cases like this that's exactly what you want, but other times you want the opposite.

For example there would probably not be much sense moving the Writer part into an equality constraint and using an even more generic instance ... => Foo (w c a). It would help GHC infer that when you're using Foo methods with a 2-parameter type constructor then it must be a Writer, but it seriously restricts your ability to add other instances (and most likely your other code will be able to establish easily enough that w is Writer).