6.2.18. Typed Holes

Typed holes are a feature of GHC that allows special placeholders written with a leading underscore (e.g., “_”, “_foo”, “_bar”), to be used as expressions. During compilation these holes will generate an error message that describes which type is expected at the hole’s location, information about the origin of any free type variables, and a list of local bindings that might help fill the hole and bindings in scope that fit the type of the hole that might help fill the hole with actual code. Typed holes are always enabled in GHC.

The goal of typed holes is to help with writing Haskell code rather than to change the type system. Typed holes can be used to obtain extra information from the type checker, which might otherwise be hard to get. Normally, using GHCi, users can inspect the (inferred) type signatures of all top-level bindings. However, this method is less convenient with terms that are not defined on top-level or inside complex expressions. Holes allow the user to check the type of the term they are about to write.

For example, compiling the following module with GHC:

f :: a -> a
f x = _

will fail with the following error:

hole.hs:2:7:
    Found hole `_' with type: a
    Where: `a' is a rigid type variable bound by
               the type signature for f :: a -> a at hole.hs:1:6
    In the expression: _
    In an equation for `f': f x = _
    Relevant bindings include
      x :: a (bound at hole.hs:2:3)
      f :: a -> a (bound at hole.hs:2:1)
    Valid hole fits include x :: a (bound at hole.hs:2:3)

Here are some more details:

  • A “Found hole” error usually terminates compilation, like any other type error. After all, you have omitted some code from your program. Nevertheless, you can run and test a piece of code containing holes, by using the -fdefer-typed-holes flag. This flag defers errors produced by typed holes until runtime, and converts them into compile-time warnings. These warnings can in turn be suppressed entirely by -Wno-typed-holes.

    The same behaviour for “Variable out of scope” errors, it terminates compilation by default. You can defer such errors by using the -fdefer-out-of-scope-variables flag. This flag defers errors produced by out of scope variables until runtime, and converts them into compile-time warnings. These warnings can in turn be suppressed entirely by -Wno-deferred-out-of-scope-variables.

    The result is that a hole or a variable will behave like undefined, but with the added benefits that it shows a warning at compile time, and will show the same message if it gets evaluated at runtime. This behaviour follows that of the -fdefer-type-errors option, which implies -fdefer-typed-holes and -fdefer-out-of-scope-variables. See Deferring type errors to runtime.

  • All unbound identifiers are treated as typed holes, whether or not they start with an underscore. The only difference is in the error message:

    cons z = z : True : _x : y
    

    yields the errors

    Foo.hs:3:21: error:
       Found hole: _x :: Bool
       Or perhaps ‘_x’ is mis-spelled, or not in scope
       In the first argument of ‘(:)’, namely ‘_x’
       In the second argument of ‘(:)’, namely ‘_x : y’
       In the second argument of ‘(:)’, namely ‘True : _x : y’
       Relevant bindings include
         z :: Bool (bound at Foo.hs:3:6)
       cons :: Bool -> [Bool] (bound at Foo.hs:3:1)
       Valid hole fits include
         z :: Bool (bound at mpt.hs:2:6)
         otherwise :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Base’))
         False :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Types’))
         True :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Types’))
         maxBound :: forall a. Bounded a => a
           with maxBound @Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Enum’))
         minBound :: forall a. Bounded a => a
           with minBound @Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Enum’))
    
    Foo.hs:3:26: error:
        Variable not in scope: y :: [Bool]
    

    More information is given for explicit holes (i.e. ones that start with an underscore), than for out-of-scope variables, because the latter are often unintended typos, so the extra information is distracting. If you want the detailed information, use a leading underscore to make explicit your intent to use a hole.

  • Unbound identifiers with the same name are never unified, even within the same function, but shown individually. For example:

    cons = _x : _x
    

    results in the following errors:

    unbound.hs:1:8:
        Found hole '_x' with type: a
        Where: `a' is a rigid type variable bound by
                   the inferred type of cons :: [a] at unbound.hs:1:1
        In the first argument of `(:)', namely `_x'
        In the expression: _x : _x
        In an equation for `cons': cons = _x : _x
        Relevant bindings include cons :: [a] (bound at unbound.hs:1:1)
    
    unbound.hs:1:13:
        Found hole: _x :: [a]
        Where: ‘a’ is a rigid type variable bound by
                the inferred type of cons :: [a]
                at unbound.hs:3:1-12
        Or perhaps ‘_x’ is mis-spelled, or not in scope
        In the second argument of ‘(:)’, namely ‘_x’
        In the expression: _x : _x
        In an equation for ‘cons’: cons = _x : _x
        Relevant bindings include cons :: [a] (bound at unbound.hs:3:1)
        Valid hole fits include
          cons :: forall a. [a]
            with cons @a
            (defined at mpt.hs:3:1)
          mempty :: forall a. Monoid a => a
            with mempty @[a]
            (imported from ‘Prelude’ at mpt.hs:1:8-10
            (and originally defined in ‘GHC.Base’))
    

    Notice the two different types reported for the two different occurrences of _x.

  • No language extension is required to use typed holes. The lexeme “_” was previously illegal in Haskell, but now has a more informative error message. The lexeme “_x” is a perfectly legal variable, and its behaviour is unchanged when it is in scope. For example

    f _x = _x + 1
    

    does not elicit any errors. Only a variable that is not in scope (whether or not it starts with an underscore) is treated as an error (which it always was), albeit now with a more informative error message.

  • Unbound data constructors used in expressions behave exactly as above. However, unbound data constructors used in patterns cannot be deferred, and instead bring compilation to a halt. (In implementation terms, they are reported by the renamer rather than the type checker.)

  • The list of valid hole fits is found by checking which bindings in scope would fit into the hole. As an example, compiling the following module with GHC:

    import Data.List (inits)
    
    g :: [String]
    g = _ "hello, world"
    

    yields the errors:

    • Found hole: _ :: [Char] -> [String]
    • In the expression: _
      In the expression: _ "hello, world"
      In an equation for ‘g’: g = _ "hello, world"
    • Relevant bindings include g :: [String] (bound at mpt.hs:6:1)
      Valid hole fits include
        lines :: String -> [String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        words :: String -> [String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        inits :: forall a. [a] -> [[a]]
          with inits @Char
          (imported from ‘Data.List’ at mpt.hs:4:19-23
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        repeat :: forall a. a -> [a]
          with repeat @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.List’))
        fail :: forall (m :: * -> *). Monad m => forall a. String -> m a
          with fail @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        return :: forall (m :: * -> *). Monad m => forall a. a -> m a
          with return @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        pure :: forall (f :: * -> *). Applicative f => forall a. a -> f a
          with pure @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        read :: forall a. Read a => String -> a
          with read @[String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘Text.Read’))
        mempty :: forall a. Monoid a => a
          with mempty @([Char] -> [String])
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
    

There are a few flags for controlling the amount of context information shown for typed holes:

-fshow-hole-constraints

When reporting typed holes, also print constraints that are in scope. Example:

f :: Eq a => a -> Bool
f x = _

results in the following message:

show_constraints.hs:4:7: error:
    • Found hole: _ :: Bool
    • In the expression: _
      In an equation for ‘f’: f x = _
    • Relevant bindings include
        x :: a (bound at show_constraints.hs:4:3)
        f :: a -> Bool (bound at show_constraints.hs:4:1)
      Constraints include Eq a (from show_constraints.hs:3:1-22)
      Valid hole fits include
        otherwise :: Bool
        False :: Bool
        True :: Bool
        maxBound :: forall a. Bounded a => a
          with maxBound @Bool
        minBound :: forall a. Bounded a => a
          with minBound @Bool

6.2.18.1. Valid Hole Fits

GHC sometimes suggests valid hole fits for typed holes, which is configurable by a few flags.

-fno-show-valid-hole-fits
Default:off

This flag can be toggled to turn off the display of valid hole fits entirely.

-fmax-valid-hole-fits=⟨n⟩
Default:6

The list of valid hole fits is limited by displaying up to 6 hole fits per hole. The number of hole fits shown can be set by this flag. Turning the limit off with -fno-max-valid-hole-fits displays all found hole fits.

-fshow-type-of-hole-fits
Default:on

By default, the hole fits show the type of the hole fit. This can be turned off by the reverse of this flag.

-fshow-type-app-of-hole-fits
Default:on

By default, the hole fits show the type application needed to make this hole fit fit the type of the hole, e.g. for the hole (_ :: Int -> [Int]), mempty is a hole fit with mempty @(Int -> [Int]). This can be toggled off with the reverse of this flag.

-fshow-docs-of-hole-fits
Default:off

It can sometime be the case that the name and type of a valid hole fit is not enough to realize what the fit stands for. This flag adds the documentation of the fit to the message, if the documentation is available (and the module from which the function comes was compiled with the -haddock flag).

-fshow-type-app-vars-of-hole-fits
Default:on

By default, the hole fits show the type application needed to make this hole fit fit the type of the hole, e.g. for the hole (_ :: Int -> [Int]), mempty :: Monoid a => a is a hole fit with mempty @(Int -> [Int]). This flag toggles whether to show a ~ (Int -> [Int]) instead of mempty @(Int -> [Int]) in the where clause of the valid hole fit message.

-fshow-provenance-of-hole-fits
Default:on

By default, each hole fit shows the provenance information of its hole fit, i.e. where it was bound or defined, and what module it was originally defined in if it was imported. This can be toggled off using the reverse of this flag.

-funclutter-valid-hole-fits
Default:off

This flag can be toggled to decrease the verbosity of the valid hole fit suggestions by not showing the provenance nor type application of the suggestions.

6.2.18.1.1. Refinement Hole Fits

When the flag -frefinement-level-hole-fits=⟨n⟩ is set to an n larger than 0, GHC will offer up a list of valid refinement hole fits, which are valid hole fits that need up to n levels of additional refinement to be complete, where each level represents an additional hole in the hole fit that requires filling in. As an example, consider the hole in

f :: [Integer] -> Integer
f = _

When the refinement level is not set, it will only offer valid hole fits suggestions:

Valid hole fits include
  f :: [Integer] -> Integer
  head :: forall a. [a] -> a
    with head @Integer
  last :: forall a. [a] -> a
    with last @Integer
  maximum :: forall (t :: * -> *).
              Foldable t =>
              forall a. Ord a => t a -> a
    with maximum @[] @Integer
  minimum :: forall (t :: * -> *).
              Foldable t =>
              forall a. Ord a => t a -> a
    with minimum @[] @Integer
  product :: forall (t :: * -> *).
              Foldable t =>
              forall a. Num a => t a -> a
    with product @[] @Integer
  sum :: forall (t :: * -> *).
          Foldable t =>
          forall a. Num a => t a -> a
    with sum @[] @Integer

However, with -frefinement-level-hole-fits=⟨n⟩ set to e.g. 1, it will additionally offer up a list of refinement hole fits, in this case:

Valid refinement hole fits include
  foldl1 (_ :: Integer -> Integer -> Integer)
    with foldl1 @[] @Integer
    where foldl1 :: forall (t :: * -> *).
                    Foldable t =>
                    forall a. (a -> a -> a) -> t a -> a
  foldr1 (_ :: Integer -> Integer -> Integer)
    with foldr1 @[] @Integer
    where foldr1 :: forall (t :: * -> *).
                    Foldable t =>
                    forall a. (a -> a -> a) -> t a -> a
  const (_ :: Integer)
    with const @Integer @[Integer]
    where const :: forall a b. a -> b -> a
  ($) (_ :: [Integer] -> Integer)
    with ($) @GHC.Types.LiftedRep @[Integer] @Integer
    where ($) :: forall a b. (a -> b) -> a -> b
  fail (_ :: String)
    with fail @((->) [Integer]) @Integer
    where fail :: forall (m :: * -> *).
                  Monad m =>
                  forall a. String -> m a
  return (_ :: Integer)
    with return @((->) [Integer]) @Integer
    where return :: forall (m :: * -> *). Monad m => forall a. a -> m a
  (Some refinement hole fits suppressed;
    use -fmax-refinement-hole-fits=N or -fno-max-refinement-hole-fits)

Which shows that the hole could be replaced with e.g. foldl1 _. While not fixing the hole, this can help users understand what options they have.

-frefinement-level-hole-fits=⟨n⟩
Default:off

The list of valid refinement hole fits is generated by considering hole fits with a varying amount of additional holes. The amount of holes in a refinement can be set by this flag. If the flag is set to 0 or not set at all, no valid refinement hole fits will be suggested.

-fabstract-refinement-hole-fits
Default:off

Valid list of valid refinement hole fits can often grow large when the refinement level is >= 2, with holes like head _ _ or fst _ _, which are valid refinements, but which are unlikely to be relevant since one or more of the holes are still completely open, in that neither the type nor kind of those holes are constrained by the proposed identifier at all. By default, such holes are not reported. By turning this flag on, such holes are included in the list of valid refinement hole fits.

-fmax-refinement-hole-fits=⟨n⟩
Default:6

The list of valid refinement hole fits is limited by displaying up to 6 hole fits per hole. The number of hole fits shown can be set by this flag. Turning the limit off with -fno-max-refinement-hole-fits displays all found hole fits.

-fshow-hole-matches-of-hole-fits
Default:on

The types of the additional holes in refinement hole fits are displayed in the output, e.g. foldl1 (_ :: a -> a -> a) is a refinement for the hole _ :: [a] -> a. If this flag is toggled off, the output will display only foldl1 _, which can be used as a direct replacement for the hole, without requiring -XScopedTypeVariables.

6.2.18.1.2. Sorting Valid Hole Fits

There are currently two ways to sort valid hole fits. Sorting can be toggled with -fsort-valid-hole-fits

-fno-sort-valid-hole-fits
Default:off

By default the valid hole fits are sorted to show the most relevant hole fits at the top of the list of valid hole fits. This can be toggled off with this flag.

-fsort-by-size-hole-fits
Default:on

Sorts by how big the types the quantified type variables in the type of the function would have to be in order to match the type of the hole.

-fsort-by-subsumption-hole-fits
Default:off

An alternative sort. Sorts by checking which hole fits subsume other hole fits, such that if hole fit a could be used as hole fits for hole fit b, then b appears before a in the output. It is more precise than the default sort, but also a lot slower, since a subsumption check has to be run for each pair of valid hole fits.