Originally I assumed the scope of a nested universal quantifier on the LHS of a function type can be determined purely syntactical, i.e. everything inside the parenthesis of (forall a. a -> b) -> Bool is within the same scope. However, this assumption is wrong:
{-# LANGUAGE RankNTypes #-}
fun :: (forall a. [a] -> b) -> Bool
fun _ = undefined
arg :: c -> c
arg _ = undefined
r = fun arg -- type variable a would escape its scope
This makes sense, because in fun b must be chosen before a and thus be fixed or independent of a. During unification the argument type c -> c would force b's instantiation with [a0] though.
So maybe scoping at the type level rather resembles that of functions at the term level, where the result value is clearly not part of the function's scope. In other words if b weren't the codomain of the function type, the type checker would pass. Unfortunately, I couldn't come up with an annotation that supports my reasoning.
A more restrictive way would be to disallow the instantiation of rigid type variables with any flexible one of the same annotation during unification. Both ways seem legitimate to me, but how does this actually work in Haskell?
Non-quantified type variables are implicitly quantified at the top-level. Your types are equivalent to
Hence, the scope of
bis the whole type.You can think of each
forall'ed variable as a kind of implicit additional type argument the function receives. I think you understand that, in a signature likethe
Intvalue is chosen by the caller offoo, while theStringvalue is chosen byfoowhen (and if) it calls the function passed as a second argument.In the same spirit,
means that
bis chosen by the caller offun, whileais chosen byfunitself.The caller can indeed pass the
btype explicitly usingTypeAnnotationsand writeAfter that, an argument of type
forall a. [a] -> Intmust be apssed, andlengthfits such polymorphic type.Since this is the call site for
funwe don't get to see where the "type argument"ais passed. That indeed can only be found in the definition offun.Well, it turns out that it's not really possible to define a
funwhich that type that makes a meaningful call to its argument (length, above). It would be if we had a slightly different signature like this:Here we can see that
f(to be bound tolengthby the call) is called twice at two distinct types. This produces two values of typebthat can then be compared to produce the finalBool.