Ever seen this in a library,
instance (var ~ AType) => ClassName (SomeType var)
and thought, “Shenanigans! Why not just have this?”
instance ClassName (SomeType AType)
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
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
_ >> return () :: Writer String (), the type of
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
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).↩︎