Ever seen this in a library,
instance (var ~ AType) => ClassName (SomeType var)
and thought, “Shenanigans! Why not just have this?”
instance ClassName (SomeType AType)
Me too!
I only learned of this solution relatively recently, and I know experienced Haskellers who also only understood this recently or still don’t. Hence this quick write up. Here’s the thought process.
We’re writing a trivial pretty printer and we’re using
Writer
. We write things like:
> execWriter (do tell "hello"; tell "world" :: Writer String ())
λ"helloworld"
Quality. But writing tell
every time is so boring! How
about we use the IsString
class so that we can just write
the string literals like this?
do "hello"; "world"
Let’s write the IsString
instance:
instance IsString (Writer String a) where
= tell fromString
What do you say, GHC?
Couldn’t match type ‘a’ with ‘()’
‘a’ is a rigid type variable bound by the instance declaration
Oh. Good point. The type of our tell
call results in
Writer String ()
. A small set back. Fine, let’s change the
instance declaration to just be ()
:
instance IsString (Writer String ()) where
= tell fromString
GHC loves it!
Let’s try using it:
> execWriter (do "hello"; "world" :: Writer String ())
λ<interactive>:42:16:
No instance for (IsString (WriterT String Identity a))
"hello"’
arising from the literal ‘The type variable ‘a’ is ambiguous
This displeases me. But it adds up given the type of
(>>)
:
(>>) :: Monad m => m a -> m b -> m b
In _ >> return () :: Writer String ()
, the type of
_
is Writer String a
, so we really need an
IsString
instance that matches that. But we
already tried that. Oh, woe!
Some people reading this will be nodding in recognition of this same problem they had while writing that perfect API that just won’t work because of this niggling issue.
Here comes the trick.1 So let’s go back to a basic instance:
data MyTuple a b = MyTuple a b
instance Show (MyTuple a b) where
show _ = "MyTuple <some value> <some value>"
Suppose I replace this instance with a new instance that has constraints:
instance (Show a,Show b) => Show (MyTuple a b) where
show (MyTuple a b) = "MyTuple " ++ show a ++ " " ++ show b
Question: Does that change whether GHC decides to pick this new version of instance over others that may be available, compared to the one above? Have a think.
The answer is: nein! The constraints of an instance don’t have anything to do with deciding whether an instance is picked from the list of instances available. Constraints only apply after GHC has already decided it’s going with this instance.
So, cognizant of this obvious-after-the-fact property, let’s use the
equality constraint that was introduced with GADTs and type families
(enabling either brings in ~
):
instance a ~ () => IsString (Writer String a) where
= tell fromString
Let’s try it:
> execWriter (do "hello" ; "world" :: Writer String ())
λ"helloworld"
This instance is picked by GHC, as we hoped, because of the
a
. The instance method also type checks, because the
constraint applies when type checking the instance methods, just like if
you write a regular declaration like:
foo :: (a ~ ()) => a
= () foo
That’s it! This crops up in a number of my own libraries and knowing
this really helped me. Here is a
real example from my lucid
library:
instance (a ~ (),Monad m) => Monoid (HtmlT m a) where
mempty = return mempty
mappend = liftM2 mappend
Hope this was helpful!
Actually, it’s a natural consequence to grokking how instance resolution works (but calling it a “trick” makes for a catchy title).↩︎