“Forall What?”
Getting GHCi to tell you the real inferred type of a subexpression
What
Put this in your .ghci
.
:def what \s -> pure ("let { it :: forall what. _; it = (" <> s <> ") } in it")
Now when you want a quick way to get the inferred type of a subexpression
inside an expression
, enter :what expression
and replace subexpression
with (subexpression :: what)
.
What?
Also, how?
If you’re trying to understand the type of an expression, particularly when dealing with a confusing type error, it’s helpful to be able to see the inferred type of a subexpression involving polymorphic terms.
For instance, earlier on in my Haskell career, one day during a code review I saw an expression like f <$> g <$> xs
, and I was surprised that it typechecked, because I remembered that the <$>
operator is left-associative, meaning that this was being parsed as (f <$> g) <$> xs
and not what my coworker had meant to write, f <$> (g <$> xs)
. In other words, fmap (fmap f g) xs
was accepted where fmap f (fmap g xs)
was intended, and I wanted to find out why. (Actually, in this case I figured it out by rephrasing the code with fmap
, but if it would come in handy later, I wanted to figure out how I could’ve figured it out.)
So my first thought was to use a type wildcard, since PartialTypeSignatures
were new at the time. In this case, asking GHCi about fmap ((fmap :: _) f g) xs
does tell us that the type is something like (a -> b) -> (t -> a) -> (t -> b)
, which contains the important bit for our purposes: (t ->)
is a Functor
. But there were other cases where this wasn’t helpful at all!
For instance, I was asked: why does foldr id
typecheck? If you ask GHCi about (foldr :: _) id
and foldr (id :: _)
, it will say, with perfect innocence and sincerity, that foldr :: (a -> b -> b) -> b -> t a -> b
and id :: a -> a
. These things you already knew—it gives you no insight. A partial type signature isn’t always quite enough to get GHCi to tell you the actual type at which a subexpression is being used. Part of the reason for this is by design: a partial signature shouldn’t over-constrain the type, so, as much as possible, the compiler will avoid saying anything it’dn’t’ve committed to if you hadn’t asked. We can sometimes wheedle the type by casually saying … :: ()
, in other words, “…, which (as you know) is a unit” and goading the typechecker into correcting us by saying, “Actually, that’s not a unit at all! It’s a —”. But that doesn’t always work: sometimes our trick backfires, and the typechecker just says “Yes, obviously.” because our annotation caused it to commit to that type: we’ve changed the outcome by trying to measure it. No good!
Fortunately, ScopedTypeVariables
give us a way to mint a fresh type variable, which, unlike some concrete type like ()
, is guaranteed not to unify with anything else. In conjunction with a partial type signature, that lets us observe type inference without disturbing it.
:{
let it :: forall what. _
it = ( (foldr :: what) id )
in it
:}
let { it :: forall what. _; it = ( (foldr :: what) id ) } in it
And as I showed earlier, we can even make this into a GHCi command.
Now the typechecker will quit beating around the bush and tell us what
it expected…but alas, ((b -> b) -> b -> b) -> b -> t (b -> b) -> b
it actually got. So sad! If we ask :what foldr (id :: what)
it laments the (b -> b) -> b -> b
that it has, which is not what
it wanted. That’s where we say “Poor thing—here, let me take those off your hands…” and make our getaway, with our pockets and purses jingling full of types.