New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove Eq
constraint and other large improvements
#12
Changes from all commits
bcef58e
2d943c6
4a0ce1d
95324fa
c22f152
cc68087
da40864
baabf6f
3615ec2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,8 @@ | |
module Control.Exception.Annotated | ||
( -- * The Main Type | ||
AnnotatedException(..) | ||
, new | ||
, exceptionWithCallStack | ||
, throw | ||
, throwWithCallStack | ||
-- * Annotating Exceptions | ||
, checkpoint | ||
|
@@ -53,7 +54,6 @@ module Control.Exception.Annotated | |
-- * Re-exports from "Control.Exception.Safe" | ||
, Exception(..) | ||
, Safe.SomeException(..) | ||
, Safe.throw | ||
, Handler (..) | ||
) where | ||
|
||
|
@@ -62,10 +62,10 @@ import Control.Exception.Safe | |
import qualified Control.Exception.Safe as Safe | ||
import Data.Annotation | ||
import Data.Maybe | ||
import qualified Data.Set as Set | ||
import Data.Typeable | ||
import GHC.Stack | ||
|
||
|
||
-- | The 'AnnotatedException' type wraps an @exception@ with | ||
-- a @['Annotation']@. This can provide a sort of a manual stack trace with | ||
-- programmer provided data. | ||
|
@@ -76,7 +76,7 @@ data AnnotatedException exception | |
{ annotations :: [Annotation] | ||
, exception :: exception | ||
} | ||
deriving (Eq, Show, Functor, Foldable, Traversable) | ||
deriving (Show, Functor, Foldable, Traversable) | ||
|
||
instance Applicative AnnotatedException where | ||
pure = | ||
|
@@ -112,30 +112,40 @@ instance (Exception exception) => Exception (AnnotatedException exception) where | |
fromException exn | ||
| Just (e :: exception) <- Safe.fromException exn | ||
= | ||
pure $ new e | ||
pure $ pure e | ||
| otherwise | ||
= | ||
Nothing | ||
|
||
-- | Attach an empty @['Annotation']@ to an exception. | ||
-- | Annotate the underlying exception with a 'CallStack'. | ||
-- | ||
-- @since 0.1.0.0 | ||
new :: e -> AnnotatedException e | ||
new = pure | ||
-- @since 0.2.0.0 | ||
exceptionWithCallStack :: (Exception e, HasCallStack) => e -> AnnotatedException e | ||
exceptionWithCallStack = | ||
AnnotatedException [callStackAnnotation] | ||
|
||
-- | Append the @['Annotation']@ to the 'AnnotatedException'. | ||
-- | ||
-- 'CallStack' is a special case - if a 'CallStack' is present in both the | ||
-- 'AnnotatedException' and the @['Annotation']@, then this will append the | ||
-- 'CallStack's in the new list and concatenate them all together. | ||
-- | ||
-- @since 0.1.0.0 | ||
annotate :: [Annotation] -> AnnotatedException e -> AnnotatedException e | ||
annotate ann (AnnotatedException anns e) = AnnotatedException (ann ++ anns) e | ||
annotate newAnnotations (AnnotatedException oldAnnotations e) = | ||
let | ||
(callStacks, other) = | ||
tryAnnotations (newAnnotations <> oldAnnotations) | ||
in | ||
foldr addCallStackToException (AnnotatedException other e) callStacks | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it make more sense for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry if this question is kinda obtuse. I know I'm missing a lot of context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's a great question, and something I waffled on a bit. It's nice to have the exposed constructor and just write It's also nice to have a notion of uniformity for how annotations are handled. It's a list. I, as a library author, don't get any special tricks that you, a library consumer, don't also have access to. So if I want to treat
data Annotations = Annotations { getAnnotations :: [Annotation], annotationsCallStack :: Maybe CallStack } But now, suppose I write this: lmao :: HasCallStack => IO a -> IO a
lmao =
checkpoint (Annotation callStack) This is going to slam the annotate :: [Annotation] -> AnnotatedException e -> AnnotatedException e
annotate anns (AnnotatedException oldAnns e) = AnnotatedException newAnns e
where
newAnns = foldr updateAnnotation oldAnns anns
updateAnnotation ann acc =
case castAnnotation ann of
Just callstack ->
acc { annotationCallStack = mergeCallStack callstack (annotationCallStack acc) }
Nothing ->
acc { getAnnotations = ann : getAnnotations acc } And this logic had to be duplicated everywhere I modify Making it flexible/extensible would be something like, uh, data TypeMap (xs :: [Type])
data SomeTypeMap where
SomeTypeMap :: TypeMap xs -> SomeTypeMap
data Annotations = Annotations { regularAnnotations :: [Annotation], specialAnnotations :: SomeTypeMap }
annotate :: Annotation -> Annotations -> Annotations
annotate ann anns =
case ann of
Annotation (a :: a) ->
case lookupTypeMap @a (specialAnnotations anns) of
Just existing ->
anns { specialAnnotations = mergeAnnotation a (specialAnnotations anns) }
Nothing ->
anns { regularAnnotations = ann : regularAnnotations anns } But then, the "special handling of annotations" is dependent on the specific value of the annotation in question, so you'd want to define something like: myThrow = throwIO (AnnotatedException myEmptyAnnotations e)
myEmptyAnnotations = Annotations [] what
where
what = SomeTypeMap
$ TypeMap.handle @CallStack mergeCallStack
$ TypeMap.handle @Foobar mergeFooBar Which also seems weird - seems weird for an exception to know how to handle it's own annotations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand your thinking. I guess I disagree in that I'm way more open to the idea of hiding the data constructors, so the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, supposing I replace Another option would be something like: class (Typeable a) => ToAnnotation a where
toAnnotation :: a -> AnnotationList
default toAnnotation :: (Show a, Typeable a) => a -> AnnotationList
toAnnotation a = mempty { annotations = [Annotation a] }
instance ToAnnotation CallStack where
toAnnotation cs = mempty { annotationCallStack = Just cs }
instance Semigroup AnnotationList where
Annotations anns mcs <> Annotations anns' mcs' =
Annotations (anns <> anns') (mergeCallStacks mcs mcs')
instance Monoid AnnotationList where
mempty = AnnotationList mempty Nothing 🤔 But then you have to write a |
||
|
||
-- | Call 'toException' on the underlying 'Exception'. | ||
-- | Call 'Safe.toException' on the underlying 'Exception'. | ||
-- | ||
-- @since 0.1.0.0 | ||
hide :: Exception e => AnnotatedException e -> AnnotatedException SomeException | ||
hide = fmap Safe.toException | ||
|
||
-- | Call 'fromException' on the underlying 'Exception', attaching the | ||
-- | Call 'Safe.fromException' on the underlying 'Exception', attaching the | ||
-- annotations to the result. | ||
-- | ||
-- @since 0.1.0.0 | ||
|
@@ -160,7 +170,7 @@ check = traverse Safe.fromException | |
-- > throw TestException `catch` \TestException -> | ||
-- > putStrLn "ok!" | ||
-- | ||
-- We can throw an exception and catch it with location. | ||
-- We can throw an exception and catch it with annotations. | ||
-- | ||
-- > throw TestException `catch` \(AnnotatedException anns TestException) -> | ||
-- > putStrLn "ok!" | ||
|
@@ -224,17 +234,23 @@ try action = | |
`catch` | ||
(\exn -> pure $ Left exn) | ||
|
||
-- | Attaches the 'CallStack' to the 'AnnotatedException' that is thrown. | ||
-- | Throws an 'Exception' and annotates it with the current 'CallStack'. | ||
-- | ||
-- The 'CallStack' will *not* be present as a 'CallStack' - it will be | ||
-- a 'CallStackAnnotation'. | ||
-- An alias for 'throwWithCallStack'. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks weird to me. Is there really no difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only difference should be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right I see - in that case can we get rid of one of them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Everyone: "Haskell code has no side effects." HasCallStack: "Hold my beer." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose we could drop one of them. I like having the alias around to keep Really, though, there's a lot that needs to be done there to make the APIs totally compatble. Kinda wish There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know that I fully agree with this idea of giving the functions the same name as some functions in some other module as a "drop in" replacement. It's almost like "reassignment by shadowing" or like dynamic scoping of variables ("extreme late binding in all things," right?). It just seems like a very error-prone practice, and all it really saves you from is having to do a regex replace. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, of course. Keeping it around just to make it slightly easier to switch over makes sense to me, even if it’s not currently a full drop-in replacement. |
||
-- | ||
-- @since 0.2.0.0 | ||
throw :: (HasCallStack, MonadThrow m, Exception e) => e -> m a | ||
throw = withFrozenCallStack throwWithCallStack | ||
|
||
-- | Attaches the 'CallStack' to the 'AnnotatedException' that is thrown. | ||
-- | ||
-- @since 0.1.0.0 | ||
throwWithCallStack | ||
:: (HasCallStack, MonadThrow m, Exception e) | ||
=> e -> m a | ||
throwWithCallStack e = | ||
Safe.throw (AnnotatedException [callStackAnnotation] e) | ||
withFrozenCallStack $ | ||
Safe.throw (AnnotatedException [callStackAnnotation] e) | ||
|
||
-- | Concatenate two lists of annotations. | ||
-- | ||
|
@@ -251,7 +267,8 @@ tryFlatten exn = | |
exn | ||
|
||
-- | Add a single 'Annotation' to any exceptions thrown in the following | ||
-- action. | ||
-- action. The 'CallStack' present on any 'AnnotatedException' will also be | ||
-- updated to include this location. | ||
-- | ||
-- Example: | ||
-- | ||
|
@@ -263,83 +280,115 @@ tryFlatten exn = | |
-- @"Foo"@. | ||
-- | ||
-- @since 0.1.0.0 | ||
checkpoint :: MonadCatch m => Annotation -> m a -> m a | ||
checkpoint ann = checkpointMany [ann] | ||
checkpoint :: (HasCallStack, MonadCatch m) => Annotation -> m a -> m a | ||
checkpoint ann = withFrozenCallStack (checkpointMany [ann]) | ||
|
||
-- | Add the current 'CallStack' to the checkpoint. This function searches any | ||
-- thrown exception for a pre-existing 'CallStack' and will not overwrite or | ||
-- replace the 'CallStack' if one is already present. | ||
-- | Add the current 'CallStack' to the checkpoint, along with the given | ||
-- annotations. This function merges 'CallStack's together, attempting to | ||
-- preserve the call site ordering as GHC does it. | ||
-- | ||
-- Primarily useful when you're wrapping a third party library. | ||
-- As of 0.2.0.0, an alias for 'checkpointMany'. | ||
-- | ||
-- @since 0.1.0.0 | ||
checkpointCallStackWith | ||
:: (MonadCatch m, HasCallStack) | ||
=> [Annotation] | ||
-> m a | ||
-> m a | ||
checkpointCallStackWith ann action = | ||
action `Safe.catch` \(exn :: SomeException) -> | ||
Safe.throw | ||
. annotate ann | ||
. addCallStackToException callStack | ||
$ case Safe.fromException exn of | ||
Just (e' :: AnnotatedException SomeException) -> | ||
case annotatedExceptionCallStack e' of | ||
Nothing -> | ||
annotate ann e' | ||
Just _preexistingCallstack -> | ||
e' | ||
Nothing -> do | ||
annotate ann $ new exn | ||
checkpointCallStackWith anns = | ||
withFrozenCallStack (checkpointMany anns) | ||
|
||
-- | Add the current 'CallStack' to the checkpoint. This function searches any | ||
-- thrown exception for a pre-existing 'CallStack' and will not overwrite or | ||
-- replace the 'CallStack' if one is already present. | ||
-- | ||
-- Primarily useful when you're wrapping a third party library. | ||
{-# DEPRECATED checkpointCallStackWith "As of 0.2.0.0 this is exactly equivalent to `checkpointMany`." #-} | ||
|
||
-- | Adds only the current 'CallStack' to the checkpoint. This function searches | ||
-- any thrown exception for a pre-existing 'CallStack' and will merge the given | ||
-- pre-existing 'CallStack' with the one on this function, in an attempt to | ||
-- preserve the actual call history. | ||
-- | ||
-- @since 0.1.0.0 | ||
checkpointCallStack | ||
:: (MonadCatch m, HasCallStack) | ||
=> m a | ||
-> m a | ||
checkpointCallStack = | ||
checkpointCallStackWith [] | ||
withFrozenCallStack (checkpoint (Annotation callStack)) | ||
|
||
-- | Add the list of 'Annotation' to any exception thrown in the following | ||
-- action. | ||
-- | ||
-- @since 0.1.0.0 | ||
checkpointMany :: (MonadCatch m) => [Annotation] -> m a -> m a | ||
checkpointMany ann action = | ||
checkpointMany :: (MonadCatch m, HasCallStack) => [Annotation] -> m a -> m a | ||
checkpointMany anns action = | ||
action `Safe.catch` \(exn :: SomeException) -> | ||
Safe.throw . annotate ann $ case Safe.fromException exn of | ||
Just (e' :: AnnotatedException SomeException) -> | ||
e' | ||
Nothing -> do | ||
new exn | ||
Safe.throw | ||
. addCallStackToException callStack | ||
. annotate anns | ||
$ case Safe.fromException exn of | ||
Just (e' :: AnnotatedException SomeException) -> | ||
e' | ||
Nothing -> do | ||
pure exn | ||
|
||
-- | Retrieves the 'CallStack' from an 'AnnotatedException' if one is present. | ||
-- | ||
-- The library maintains an internal check that a single 'CallStack' is present | ||
-- in the list, so this only returns the first one found. If you have added | ||
-- a 'CallStack' directly to the @['Annotation']@, then this will likely break. | ||
-- | ||
Comment on lines
+334
to
+337
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, sorry for sounding like a broken record here, but this just corroborates my earlier thinking. Wouldn't it really make more sense for |
||
-- @since 0.1.0.0 | ||
annotatedExceptionCallStack :: AnnotatedException exception -> Maybe CallStack | ||
annotatedExceptionCallStack exn = | ||
let (stacks, _rest) = callStackInAnnotations (annotations exn) | ||
let (stacks, _rest) = tryAnnotations (annotations exn) | ||
in listToMaybe stacks | ||
|
||
-- | Adds a 'CallStack' to the given 'AnnotatedException'. This function will | ||
-- search through the existing annotations, and it will not add a second | ||
-- 'CallStack' to the list. | ||
-- 'CallStack' to the list. Instead, it will append the contents of the given | ||
-- 'CallStack' to the existing one. | ||
-- | ||
-- This mirrors the behavior of the way 'HasCallStack' actually works. | ||
-- | ||
-- @since 0.1.0.0 | ||
addCallStackToException | ||
:: CallStack | ||
-> AnnotatedException exception | ||
-> AnnotatedException exception | ||
addCallStackToException cs exn = | ||
case annotatedExceptionCallStack exn of | ||
Nothing -> | ||
annotate [callStackToAnnotation cs] exn | ||
Just _ -> | ||
exn | ||
addCallStackToException cs (AnnotatedException annotations' e) = | ||
AnnotatedException anns' e | ||
where | ||
anns' = go annotations' | ||
-- not a huge fan of the direct recursion, but it seems easier than trying | ||
-- to finagle a `foldr` or something | ||
Comment on lines
+360
to
+361
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it'd be neat if the compiler could verify that at least one branch/equation doesn't make a recursive call. that still wouldn't guarantee termination, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also wouldn't catch loops in mutual recursion. |
||
go [] = | ||
[Annotation cs] | ||
go (ann : anns) = | ||
case castAnnotation ann of | ||
Just preexistingCallStack -> | ||
mergeCallStack preexistingCallStack cs : anns | ||
Nothing -> | ||
ann : go anns | ||
|
||
-- we want to merge callstack but not duplicate entries | ||
mergeCallStack pre new = | ||
Annotation | ||
$ fromCallSiteList | ||
$ fmap (fmap fromSrcLocOrd) | ||
$ ordNub | ||
$ fmap (fmap toSrcLocOrd) | ||
$ getCallStack pre <> getCallStack new | ||
|
||
toSrcLocOrd (SrcLoc a b c d e f g) = | ||
(a, b, c, d, e, f, g) | ||
fromSrcLocOrd (a, b, c, d, e, f, g) = | ||
SrcLoc a b c d e f g | ||
|
||
-- | Remove duplicates but keep elements in order. | ||
-- O(n * log n) | ||
-- Vendored from GHC | ||
ordNub :: Ord a => [a] -> [a] | ||
ordNub = go Set.empty | ||
where | ||
go _ [] = [] | ||
go s (x:xs) | ||
| Set.member x s = go s xs | ||
| otherwise = x : go (Set.insert x s) xs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish there were a way to expose something like this for testing without influencing the library interface, other than like CPP.