haskell / core-libraries-committee

95 stars 15 forks source link

Exception backtrace proposal: Part 1: Annotations #200

Closed bgamari closed 10 months ago

bgamari commented 1 year ago

Tracking ticket: #164

This proposal attempts to summarise the interface design of the exception annotation scheme proposed in GHC Proposal #330. Specifically, this proposal covers the changes described in sections 2.1, 2.3, 2.4, and 2.5.

Note that the GHC Proposal is free-standing; no reading of the discussion which lead to its current accepted state should be necessary to understand its contents. Consequently, to avoid repetition I will refer back to the GHC Proposal instead of repeating myself here. I will, however, attempt to give some color to the interfaces by providing typical usage examples where necessary. However, the GHC Proposal is to be considered the canonical definition of the interfaces; in particular, section 2 and its subsections precisely captures the changes proposed in base.

Goal

The goal of this work is to allow users to more easily locate the causes of exceptions within their program. In particular, we note that when troubleshooting the context in which an exception occurred can be just as important in identifying the cause as the particulars of the event itself. For this reason, "cause" here may mean many things:

Since the particular information necessary to determine the "cause" of an exception can be quite domain-dependent, we propose a general-purpose annotation mechanism following the model of the annotated-exception library. This mechanism allows the user to attach dynamically-typed annotations to exceptions, which can be later inspected and displayed.

With this general-purpose mechanism in hand, we define a set of annotation types for capturing various backtrace flavours (namely, HasCallStack stacks, profiling cost-centre-stacks, DWARF execution stacks, and native Haskell execution stacks).

Exception annotations

The proposed annotation mechanism is introduced by extending the existing SomeException type, which represents the root of the exception hierarchy (readers wanting a refresher for GHC's exception system are referred to "An Extensible Dynamically-Typed Hierarchy of Exceptions"). This type is currently defined as:

-- Currently in Control.Exception
data SomeException = forall e. (Exception e) => SomeException e

We propose (section 2.4) to add an additional implicit parameter context to the SomeException data constructor, following the model of HasCallStack:

-- In Control.Exception
data SomeException = forall e. (Exception e, HasExceptionContext) => SomeException e

type HasExceptionContext = (?exceptionContext :: ExceptionContext)

someExceptionContext :: SomeException -> ExceptionContext

Since this addition would represent a breaking change, we propose (section 8.6) to provide defaulting logic in the typechecker, again following the model of HasTypeStack.

As described in section 2.3, ExceptionContext is an abstract, order-preserving collection of annotations. As a concrete representation we propose to use a simple List:

-- In Control.Exception.Context
newtype ExceptionContext = ExceptionContext [SomeExceptionAnnotation]
instance Monoid ExceptionContext
instance Semigroup ExceptionContext

Exception annotations are dynamically-typed values distinguished by the ExceptionAnnotation typeclass (section 2.1):

-- In Control.Exception.Annotation
data SomeExceptionAnnotation where
    SomeExceptionAnnotation ::
      forall a. (ExceptionAnnotation a) => a -> SomeExceptionAnnotation

Where the ExceptionAnnotation class provides for rendering of annotations to a user-facing string (following the model of displayException; section 2.1):

-- In Control.Exception.Annotation
class (Typeable a) => ExceptionAnnotation a where
  displayExceptionAnnotation :: a -> String

  default displayExceptionAnnotation :: Show a => a -> String
  displayExceptionAnnotation = show

We provide a few combinators for manipulating exception context (section 2.3):

-- In Control.Exception.Context
emptyExceptionContext :: ExceptionContext
addExceptionAnnotation :: ExceptionAnnotation a => a -> ExceptionContext -> ExceptionContext
getExceptionAnnotations :: ExceptionAnnotation a => ExceptionContext -> [a]
getAllExceptionAnnotations :: ExceptionContext -> [SomeExceptionAnnotation]

Adding annotations to exceptions

Attaching annotations to an exception may be accomplished using an introduced addExceptionContext function:

-- In Control.Exception
addExceptionContext :: (ExceptionAnnotation a)
                    => a -> SomeException -> SomeException

It is anticipated that users may frequently want to run an IO action, ensuring that any exceptions thrown therein are given an annotation. This can be accomplished using the annotateIO function (section 2.4):

-- In Control.Exception
annotateIO :: ExceptionAnnotation a => a -> IO r -> IO r

Showing annotations

Naturally displayExceptionAnnotation can be used to display each of the annotations contained within an ExceptionContext (section 2.3):

-- In Control.Exception.Context
displayExceptionContext :: ExceptionContext -> String
displayExceptionContext (ExceptionContext anns0) = go anns0
  where
    go (SomeExceptionAnnotation ann : anns) = displayExceptionAnnotation ann ++ "\n" ++ go anns
    go [] = "\n"

To make annotations visible to users, we propose to:

  1. Use displayException in GHC's top-level exception handler (CLC#198)
  2. Teach the displayException implementation of SomeException to display attached ExceptionContext via displayExceptionContext (section 2.10)

Providing exception context to handlers

Typically when users use handle or catch they do so against a concrete exception type, not SomeException. Since this pattern would prevent access to ExceptionContext, we propose adding a convenient wrapper to expose an arbitrary exception with its context. The Exception instance of this wrapper allows it to be used in both throwing and catching contexts:

-- In Control.Exception
data ExceptionWithContext a = ExceptionWithContext ExceptionContext a

instance Show a => Show (ExceptionWithContext a)

instance Exception a => Exception (ExceptionWithContext a) where
    toException (ExceptionWithContext ctxt e) = SomeException e
      where ?exceptionContext = ctxt
    fromException se = do
        e <- fromException se
        return (ExceptionWithContext (someExceptionContext se) e)
    displayException = displayException . toException

Usage

In the case of a server application, the author may wish to augment exceptions thrown within a request handler with information about the request being handled. This can be achieved with the above interfaces as follows:

data Request
instance ExceptionAnnotation Request where
    displayExceptionAnnotation req =
        "While handling request " ++ show req

handler :: Request -> IO ()
handler req = annotateIO req $ do ...

This example is representative of what we believe will be typically needed by end-users.

Far fewer users (e.g. structured and application-specific logging infrastructure) will need to directly inspect exceptions' context.

Migration

Thanks to the default behavior described in Section 8.6, the HasExceptionContext constraints introduced in SomeException should be discharged automatically.

All other changes are strict additions which should require no migration effort on the part of library authors or application developers.

parsonsmatt commented 10 months ago

+1

Bodigrim commented 10 months ago

Thanks all, that's enough votes to approve.

bgamari commented 9 months ago

Thank you all! I'm looking forward to continuing this process with Part 2.