Skip to content

Conversation

@kaol
Copy link
Contributor

@kaol kaol commented Sep 26, 2025

The exceptions package is a transitive dependency of this package via semigroupoids so these instances can't be there.

@RyanGlScott
Copy link
Collaborator

Thanks for the PR!

I think adding a MonadThrow instance is quite reasonable. For symmetry with the MonadTrans instance for ContT, I wonder if we should instead define Codensity's MonadThrow instance as:

instance MonadThrow m => MonadThrow (Codensity m) where
  throwM = lift . throwM

(I'm not sure if this implementation is meaningfully different from the one you've provided, but it wouldn't surprise me if there were a way to observe the difference between the two instances in certain corner cases.)

I'm more reluctant to add a MonadCatch instance, however. ContT (which Codensity is very similar to) intentionally does not have a MonadCatch instance because it can lead to extremely unusual behavior (see this link) where an exception handler may be run multiple times (or not at all) depending on how the continuation is used. Here is an adaptation of the example in the link that uses Codensity instead of ContT:

module Main where

import Control.Monad.Catch
import Control.Monad.Codensity
import Control.Monad.IO.Class

bracket_' :: MonadCatch m
          => m a  -- ^ computation to run first (\"acquire resource\")
          -> m b  -- ^ computation to run last when successful (\"release resource\")
          -> m b  -- ^ computation to run last when an exception occurs
          -> m c  -- ^ computation to run in-between
          -> m c  -- returns the value from the in-between computation
bracket_' before after afterEx thing = do
   _ <- before
   r <- thing `onException` afterEx
   _ <- after
   return r

f :: Codensity IO String
f = do
     bracket_' (say "acquired") (say "released-successful") (say "released-exception") (say "executed")
     say "Hello!"
     () <- error "error"
     return "success"
   where
     say = liftIO . putStrLn

main :: IO ()
main = runCodensity f (return . Right @String @String) >>= print

When run, this produces:

acquired
executed
released-successful
Hello!
released-exception
Bug.hs: error
CallStack (from HasCallStack):
  error, called at Bug.hs:23:12 in main:Main

Notice that the exception handler is run twice, once after a successful computation within bracket_', and once again for error, even though the call to error doesn't occur within a bracket_' at all.

@kaol
Copy link
Contributor Author

kaol commented Sep 27, 2025

I see, thank you for the explanation. I'm not seeing a way around this issue and I suspect it's a fundamental flaw with this approach. I could still edit this pull request to be only about MonadThrow if you like but MonadCatch was the one I was really interested in.

Luckily, in my case it was easy enough to move things around to have the exception be caught in a section where I was operating within the underlying monad (kaol/servant-core-miso-client@b411fd6 if you are curious).

@RyanGlScott
Copy link
Collaborator

Up to you! I think a MonadThrow instance would be nice to have for symmetry with the ContT instance.

@kaol kaol changed the title Add MonadCatch and MonadThrow instances for Codensity Add MonadThrow instance for Codensity Sep 27, 2025
@kaol
Copy link
Contributor Author

kaol commented Sep 27, 2025

Okay, I force pushed a version with the MonadThrow instance only, for your consideration.

Copy link
Collaborator

@RyanGlScott RyanGlScott left a comment

Choose a reason for hiding this comment

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

Thank you!

@RyanGlScott RyanGlScott merged commit fb3d97f into ekmett:master Sep 27, 2025
11 checks passed
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.

2 participants