4.2 Quantifiers
4.2.3 Visibility
A quantifiee may be either visible or invisible. The argument used to instantiate a visible quantifiee appears in the Haskell source; the argument used to instantiate an invisible quantifiee is elided.
Today’s Haskell uses (→) for visible quantification. That is, when we pass an ordinary function an argument, the argument is visible in the Haskell source. For example, the 3in pred 3 is visible.
On the other hand, today’s ∀ and (⇒) are invisible quantifiers. When we call
id True, the a in the type of id is instantiated atBool, but Bool is elided in the call
id True. During type inference, GHC uses unification to discover that the correct argument to use for a isBool.
Invisible arguments specified with (⇒) are constraints. Take, for example, show ::
∀ a. Show a ⇒ a → String. The show function properly takes 3 arguments: the
∀-quantified type variable a, the (⇒)-quantified dictionary for Show a (see Section 2.1 if this statement surprises you), and the (→)-quantified argument of type a. However, we use show as, say,show True, passing only one argument visibly. The ∀a argument is discovered by unification to beBool, but the Show a argument is discovered using a different mechanism: instance solving and lookup. (See the work of Vytiniotis et al. [99] for the algorithm used.) We thus must be aware that invisible arguments may use different mechanisms for instantiation.
Dependent Haskell offers both visible and invisible forms of ∀ and Π; the invisible forms instantiate only via unification. Dependent Haskell retains, of course, the invisible quantifier (⇒), which is instantiated via instance lookup and solving. Finally, note that visibility is a quality only of source Haskell. All arguments are always “visible” in
Pico.
It may be helpful to compare Dependent Haskell’s treatment of visibility to that in other languages; see Section 8.6.
4.2.3.1 Visibility overrides
It is often desirable when using rich types to override a declared visibility specification. That is, when a function is declared to have an invisible parameter a, a call site may wish to instantiate a visibly. Conversely, a function may declare a visible parameter
b, but a caller knows that the choice for b can be discovered by unification and so wishes to omit it at the call site.
Instantiating invisible parameters visibly Dependent Haskell adopts the @. . . syntax of Eisenberg et al. [33] to instantiate any invisible parameter visibly, whether it is a type or not. Continuing our example with id, we could write id @Bool True
instead ofid True. This syntax works in patterns, expressions, and types. In patterns, the choice of@conflicts with as-patterns, such as using the patternlist@(x:xs)to bind
written without whitespace. I thus use the presence of whitespace before the @ to signal the choice between an as-pattern and a visibility override.29 Dictionaries cannot be named in Haskell, so this visibility override skips over any constraint arguments.
Omitting visible parameters The function replicate:: Π (n::Nat)→a →Vec a n
from Section 3.1.1.3 creates a length-indexed vector of length n, where n is passed in as the first visible argument. (The true first argument is a, which is invisible and elided from the type.) However, the choice forn can be inferred from the context. For example:
theSimons ::Vec String 2
theSimons =replicate 2"Simon"
In this case, the two uses of 2 are redundant. We know from the type signature that the length of theSimons should be 2. So we can omit the visible parameter n when calling replicate:
theSimons’ ::Vec String 2
theSimons’ =replicate "Simon"
The underscore tells GHC to infer the missing parameter via unification.
The two overrides can usefully be combined, when we wish to infer the instantiation of some invisible parameters but then specify the value for some later invisible parameter. Consider, for example,coerce ::∀a b.Coercible a b ⇒a →b. In the call
coerce (MkAge 3) (where we have newtype Age =MkAge Int), we can infer the value for a, but the choice for b is a mystery. We can thus say coerce@_@Int (MkAge 3), which will convert MkAge 3to an Int.
The choice of syntax for omitting visible parameters conflicts somewhat with the feature of typed holes, whereby a programmer can leave out a part of an expression, replacing it with an underscore, and then get an informative error message about the type of expression expected at that point in the program. (This is not unlike Agda’s
sheds feature or Idris’smetavariables feature.) However, this is not a true conflict, as an uninferrable omitted visible parameter is indeed an error and should be reported; the error report is that of a typed hole. Depending on user feedback, this override of the underscore symbol may be hidden behind a language extension or other compiler flag.
29This perhaps-surprising decision based on whitespace is regrettable, but it has company. The
symbol$ can mean an ordinary, user-defined operator when it is followed by a space but a Template Haskell splice when there is no space. The symbol. can mean an ordinary, user-defined operator when it is preceded by a space but indicate namespace resolution when it is not. Introducing these oddities seems a good bargain for concision in the final language.