6.4.17. Custom compile-time errors

When designing embedded domain specific languages in Haskell, it is useful to have something like error at the type level. In this way, the EDSL designer may show a type error that is specific to the DSL, rather than the standard GHC type error.

For example, consider a type class that is not intended to be used with functions, but the user accidentally used it at a function type, perhaps because they missed an argument to some function. Then, instead of getting the standard GHC message about a missing instance, it would be nicer to emit a more friendly message specific to the EDSL. Similarly, the reduction of a type-level function may get stuck due to an error, at which point it would be nice to report an EDSL specific error, rather than a generic error about an ambiguous type.

To solve this, GHC provides a single type-level function,

type family TypeError (msg :: ErrorMessage) :: k

along with a small type-level language (via DataKinds) for constructing pretty-printed error messages,

-- ErrorMessage is intended to be used as a kind
data ErrorMessage =
      Text Symbol                        -- Show this text as is
    | forall t. ShowType t               -- Pretty print a type
    | ErrorMessage :<>: ErrorMessage     -- Put two chunks of error message next to each other
    | ErrorMessage :$$: ErrorMessage     -- Put two chunks of error message above each other

in the GHC.TypeLits module.

For instance, we might use this interface to provide a more useful error message for applications of show on unsaturated functions like this,

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

import GHC.TypeLits

instance TypeError (Text "Cannot 'Show' functions." :$$:
                    Text "Perhaps there is a missing argument?")
         => Show (a -> b) where
   showsPrec = error "unreachable"

main = print negate

Which will produce the following compile-time error,

Test.hs:12:8: error:
    • Cannot 'Show' functions.
      Perhaps there is a missing argument?
    • In the expression: print negate
      In an equation for ‘main’: main = print negate