If there is one piece of advice about type inference I think everyone should hear it's absolutely this
27 July 2023Be very explicit about the difference between type variables, unification variables and skolems
Type variables are always bound by
forall
s and represent variables that can be instantiated to
any possible concrete type. For example,
forall a. a -> a
can be instantiated to
Int -> Int
, String -> String
or
Bool -> Bool
Unification variables are placeholders that stand for as of yet unknown concrete types. These usually originate from usages of polymorphic types (where type variables are instantiated with fresh(!!!) unification variables at every use site, since every use site instantiates the type variable to a possibly different concrete type, but which one that is might not be known yet), but they are more generally useful in places where a type is needed that might not be fully known yet (e.g. unannotated lambda parameters or recursive let bindings)
Skolems on the other hand are type constants that
are only equal to themselves. These are needed when checking
against polymorphic types. The idea is that a function with type
forall a. a -> a
cannot make any assumptions about the
type of its parameter, which is ensured by checking it against the type
#a -> #a
(where #a
is a fresh skolem).
These three are often confused since they could all reasonably be called “type variables”, but doing so will only bring you pain. They are distinct concepts and should really be treated as such.
Unfortunately GHC is not great at this. In error messages,
unification variables are usually displayed with a trailing
number (a0
), although something like a1
can
either be a unification variable or a skolem if just a
would be ambiguous. The only way to tell which one it is is by looking
for 'a1' is a rigid type variable bound by...
(you know, in
the section of the error that noone actually reads because it’s massive
and almost never useful)