I was recently working on some Haskell code (for research, with Jack Hughes) and happened to be using a monoid (via the Monoid
type class) and I was rushing. I accidentally wrote x `mempty` y
instead of x `mappend` y
. The code with mempty
type checked and compiled, but I quickly noticed some tests giving unexpected results. After a pause, I checked the recent diff and noticed this mistake, but I had to think for a moment about why this mistake was not leading to a type error. I thought this was an interesting little example of how type class instance resolution can sometimes trip you up in Haskell, and how to uncover what is going on. This also points to a need for GHC to explain its instance resolution, something others have thought about; I will briefly mention some links at the end.
The nub of the problem
In isolation, my mistake was essentially this:
whoops :: Monoid d => d -> d -> d
whoops x y = mempty x y -- Bug here. Should be "mappend x y"
So, given two parameters of type d
for which there is a monoid structure on d
, we use both parameters as arguments to mempty
.
Recall the Monoid
type class is essentially:
class Monoid d where mempty :: d mappend :: d -> d -> d
(note that Monoid
also has a derived operation mconcat
and is now decomposed into Semigroup
and Monoid
but I elide that detail here, see https://hackage.haskell.org/package/base-4.14.0.0/docs/Prelude.html#t:Monoid).
We might naively think that whoops
would therefore not type check since we do not know that d
is a function type. However, whoops
is well-typed and evaluating
whoops [1,2,3] [4,5,6]
returns []
. If the code had been as I intended, (using mappend
here instead of mempty
) then we would expect [1,2,3,4,5,6]
according to the usual monoid on lists.
The reason this is not a type error is because of GHC’s instance resolution and the following provided instance of `Monoid`:
instance Monoid b => Monoid (a -> b) where mempty = \_ -> mempty mappend f g = \x -> f x `mappend` g x
That is, functions are monoids if their domain is a monoid with mempty
as the constant function returning the mempty
element of b
and mappend
as the pointwise lifting of a monoid to a function space.
In this case, we can dig into what is happening by compiling1 with --ddump-ds-preopt
and looking at GHC’s desugared output before optimisation, where all the type class instances have been resolved. I’ve cleaned up the output a little (mostly renaming):
whoops :: forall d. Monoid d => d -> d -> d whoops = \ (@ d) ($dMonoid :: Monoid d) -> let $dMonoid_f :: Monoid (d -> d) $dMonoid_f = GHC.Base.$fMonoid-> @ d @ d $dMonoid } $dMonoid_ff :: Monoid (d -> d -> d) $dMonoid_ff = GHC.Base.$fMonoid-> @ (d -> d) @ d $dMonoid_f } in \ (x :: d) (y :: d) -> mempty @ (d -> d -> d) $dMonoid_ff x y
The second line shows whoops
has a type parameter (written @ d
) and the incoming dictionary $dMonoid
representing the Monoid d
type class instance (type classes are implemented as data types called dictionaries, and I use the names $dMonoidX
for these here).
Via the explicit type application (of the form @ t
for a type term t
) we can see in the last line that mempty
is being resolved at the type d -> d -> d
with the monoid instance Monoid (d -> d -> d)
given here by the $dMonoid_ff
construction just above. This is in turn derived from the Monoid (d -> d)
given by the dictionary construction $dMonoid_f
just above that. Thus we have gone twice through the lifting of a monoid to a function space, and so our use of mempty
here is:
mempty @ (d -> d -> d) $dMonoid_ff = \_ -> (\_ -> mempty @ $dMonoid)
thus
mempty @ (d -> d -> d) $dMonoid_ff x y = mempty @ d $dMonoid
That’s why the program type checks and we see the mempty
element of the original intended monoid on d
when applying whoops
to some arguments and evaluating.
Epilogue
I luckily spotted my mistake quite quickly, but this kind of bug can be a confounding experience for beginners. There has been some discussion about extending GHCi with a feature allowing users to ask GHC to explain its instance resolution. Michael Sloan has a nice write up discussing the idea and there is a GHC ticket proposing by Icelandjack something similar which seems like it would work well in this context where you want to ask what the instance resolution was for a particular expression. There are many much more confusing situations possible that get hidden by the implicit nature of instance resolution, so I think this would be a very useful feature, for beginner and expert Haskell programmers alike. And it certainly would have explained this particular error quickly to me without me having to scribble on paper and then check --ddump-ds-preopt
to confirm my suspicion.
Additional: I should also point out that this kind of situation could be avoided if there were ways to scope, import, and even name type class instances. The monoid instance Monoid b => Monoid (a -> b)
is very useful, but having it in scope by default as part of base was really the main issue here.
1 To compile, put this in a file with a main
stub, e.g.
main :: IO () main = return ()