Skip to content
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

Add HasCallStack to functions (resolves #40) #41

Merged
merged 4 commits into from Jun 26, 2023

Conversation

tbidne
Copy link
Contributor

@tbidne tbidne commented Feb 16, 2023

Resolves #40. I'm putting this up now for visibility, though it's probably best to wait until GHC 9.6 can be added to CI so the new code can actually be compiled on CI (9.6 is the first GHC using exceptions >= 0.10.6 that this PR utilizes).

Couple notes:

  1. In addition to the MIN_VERSION_base(4,9,0) guard for HasCallStack, I added a condition for MIN_VERSION_exceptions(0,10,6), since there is no reason to add HasCallStack for any lower versions (also it would trip -Wredundant-constraints, though that doesn't appear to be on).

  2. It is not entirely clear to me when it is best to use withFrozenCallStack, so I followed the original PR's lead. The following table summarizes the situation:

    • LHS listing exceptions functions using withFrozenCallStack
    • RHS is either the safe-exceptions counterpart, or a note that the original exceptions function was re-exported, so we do not have to do anything.

    Additionally, safe-exceptions has a few functions that do not exist in exceptions, yet seem they like they should also use withFrozenCallStack, so I added those as well, and listed them as N/A.

    Click to expand table
    exceptions safe-exceptions
    mask_ Re-exported
    uninterruptibleMask_ Re-exported
    catchAll catchAny
    catchIOError Re-exported
    catchIf N/A
    catchJust catchJust
    handle handle
    handleIOError Re-exported
    handleAll handleAny
    handleIf N/A
    handleJust handleJust
    try try
    tryJust tryJust
    catches catches
    onException onException
    onError N/A
    bracket bracket
    bracket_ bracket_
    finally finally
    bracketOnError Re-exported
    N/A catchIO
    N/A catchDeep
    N/A catchAnyDeep
    N/A handleIO
    N/A handleAny
    N/A handleDeep
    N/A handleAnyDeep
    N/A tryIO
    N/A tryAny
    N/A tryDeep
    N/A tryAnyDeep
    N/A withException
    N/A bracketOnError_
    N/A bracketWithError
    N/A catchesDeep

Perhaps @parsonsmatt would be kind enough to glance over the withFrozenCallStack logic?

Copy link

@parsonsmatt parsonsmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few places that I'd change the frozen call stack behavior. It's a tricky function to apply correctly - IMO it's almost easier to write with parens explicitly to make it clearer what's happening.

withFrozenCallStack foo bar baz
withFrozenCallStack $ foo bar baz
(withFrozenCallStack foo) bar baz
withFrozenCallStack (foo bar baz)

Usually these are equivalent, but the implicit param obscures how these differ.

Comment on lines +151 to 152
throw :: HAS_CALL_STACK => (C.MonadThrow m, Exception e) => e -> m a
throw = C.throwM . toSyncException

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could put a withFrozenCallStack here, which would "hide" that Control.Monad.Catch.throwM is the underlying throwing mechanism. Not sure if that's desired or not.

src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
Comment on lines 410 to 411
withException :: HAS_CALL_STACK => (C.MonadMask m, E.Exception e) => m a -> (e -> m b) -> m a
withException thing after = withFrozenCallStack $ C.uninterruptibleMask $ \restore -> do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtle thing here - withFrozenCallStack $ is going to freeze the CallStack for thing and after, while withFrozenCallStack C.uninterruptibleMask is going to only freeze the CallStack for that function. I suspect we want the former, though I also suspect that it's going to play weirdly with the types. May be worth not freezing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we want the former

Did you mean to say latter here, by any chance? If so I agree with your comment: withFrozenCallStack C.uninterruptibleMask seems closer to what we want (freeze the handler only), and it is consistent with other functions. And you are right about it misbehaving. Naively changing it to

withFrozenCallStack C.uninterruptibleMask $ \restore

gives

• Couldn't match type: m a -> m a
                 with: forall a1. m a1 -> m a1
  Expected: (forall a1. m a1 -> m a1) -> m a
    Actual: (m a -> m a) -> m a

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact, ghc can be placated with -XImpredicativeTypes

-- requires -XImpredicativeTypes
uninterruptibleMaskFrozen :: forall m b. C.MonadMask m => ((forall a. m a -> m a) -> m b) -> m b
uninterruptibleMaskFrozen = withFrozenCallStack C.uninterruptibleMask

withException thing after = uninterruptibleMaskFrozen $ \restore -> do ...

I'm not sure how to do this without it, however (-XRankNTypes is not enough).

src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
src/Control/Exception/Safe.hs Outdated Show resolved Hide resolved
- Add withFrozenCallStack to simple aliases.
- Remove HasCallStack from throwTo as it is unused.
- Rewrite functions such that withFrozenCallStack is applied to the
  handler only (e.g. catch), not the action.
- Remove withFrozenCallStack from withException as desired
  semantics are unclear.
@tbidne
Copy link
Contributor Author

tbidne commented May 4, 2023

@parsonsmatt Thanks for the review! Indeed, "withFrozenCallStack best practices" are not totally clear to me, so your comments are super helpful. I implemented most of your requests. Some comments:

  1. I am indifferent on throw.
  2. I removed withFrozenCallStack from withException due to the uncertainty + type problems. Better to leave some noise in than to accidentally throw useful information away. Of course I am open to adding it back if there is a good argument in its favor.

The same problem for withException (described here) exists for finally, so whatever is decided for withException, the same should apply to finally.

Edit: I went ahead and made the same change to finally (i.e. no withFrozenCallStack).

Removes withFrozenCallStack from finally for consistency with withException.
Adds explanatory comment.
Also remove windows CI workaround now that issue has been resolved.
@tbidne
Copy link
Contributor Author

tbidne commented Jun 23, 2023

Hey @snoyberg, I believe this is ready for your review, now that stackage nightly has 9.6 and thus this PR can be tested on CI.

In addition to the HasCallStack changes, I took the liberty of:

  • Adding recent snapshots to CI and removing your windows workaround as the referenced issue is marked resolved (this passed on my fork, though CI needs to be authorized to run here).
  • Bumped the patch.

Let me know if you want something else. Thanks!

Copy link
Member

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. @parsonsmatt mind reviewing before I merge/release?

Copy link

@parsonsmatt parsonsmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! One curiosity re deep subumption on the withFrozenCallStack note, but I'd be happy to just merge/release as is

onException :: HAS_CALL_STACK => C.MonadMask m => m a -> m b -> m a
onException thing after = withFrozenCallStack withException thing (\(_ :: SomeException) -> after)

-- Note: [withFrozenCallStack impredicativity]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parsonsmatt Good idea! Unfortunately, I just tried it and received the same error:

{-# LANGUAGE DeepSubsumption #-}
...
withException thing after = withFrozenCallStack C.uninterruptibleMask $ ...

-- src/Control/Exception/Safe.hs:441:49: error: [GHC-83865]
--     • Couldn't match type: m a -> m a
--                      with: forall a1. m a1 -> m a1
--       Expected: (forall a1. m a1 -> m a1) -> m a
--         Actual: (m a -> m a) -> m a
--     • In the first argument of ‘withFrozenCallStack’, namely
--         ‘C.uninterruptibleMask’

This however inspired me to try manually eta-expanding All The Things, and it actually worked without any new extensions!

diff --git a/src/Control/Exception/Safe.hs b/src/Control/Exception/Safe.hs
index 0c4b5e9..8872d3c 100644
--- a/src/Control/Exception/Safe.hs
+++ b/src/Control/Exception/Safe.hs
@@ -437,7 +437,7 @@ onException thing after = withFrozenCallStack withException thing (\(_ :: SomeEx
 --
 -- @since 0.1.0.0
 withException :: HAS_CALL_STACK => (C.MonadMask m, E.Exception e) => m a -> (e -> m b) -> m a
-withException thing after = C.uninterruptibleMask $ \restore -> do
+withException thing after = withFrozenCallStack (\io -> C.uninterruptibleMask (\r -> io r)) $ \restore -> do
     fmap fst $ C.generalBracket (pure ()) cAfter (const $ restore thing)
   where
     -- ignore the exception from after, see bracket for explanation
@@ -464,7 +464,7 @@ bracket_ before after thing = withFrozenCallStack bracket before (const after) (
 --
 -- @since 0.1.0.0
 finally :: HAS_CALL_STACK => C.MonadMask m => m a -> m b -> m a
-finally thing after = C.uninterruptibleMask $ \restore -> do
+finally thing after = withFrozenCallStack (\io -> C.uninterruptibleMask (\r -> io r)) $ \restore -> do
     fmap fst $ C.generalBracket (pure ()) cAfter (const $ restore thing)
   where
     -- ignore the exception from after, see bracket for explanation

I missed this when I was experimenting earlier. I am curious what others think of this idea.

@snoyberg snoyberg merged commit ecd3627 into fpco:master Jun 26, 2023
23 of 24 checks passed
@tbidne
Copy link
Contributor Author

tbidne commented Jun 26, 2023

Thanks everyone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add HasCallStack to functions
3 participants