-
Notifications
You must be signed in to change notification settings - Fork 374
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
MonadCatch, MonadMask instances for HandlerFor #1533
Comments
What's the motivating use case for this? I've been on the fence about such instances in a few libraries now. |
Mostly a lot of existing code that uses -- | Helper to handle failure in n handler in a consistent manner.
--
-- On failure a "badRequest400" status is returned with the error message
-- returned as a "FailureResponse". Only failures tagged as public, "FailurePublic"
-- form part of the error message. All the failures, both public and private, are logged as an
-- error.
--
--
handleFailure
:: forall e em m a r. (MonadHandler m, MonadLogger m, ToJSON em, ToFailureMessage em, ToTypedContent r)
=> Text -- ^ The location used in the error message log
-> (e -> em) -- ^ Convert the failure error types that have a "ToJSON" and "ToFailureMessage" types
-> (a -> r) -- ^ Convert the resultant value of the handler to a "ToTypedContent"
-> ExceptT [Failure e] m a -- ^ Run the handler wrapped in an ExceptT that can fail with "Failure e"
-> m r
handleFailure loc_ printe_ mkResp_ act_ = do
eresult <- runExceptT act_
case eresult of
Left e -> do
logErrorNS loc_ (printErrors e)
sendStatusJSON
badRequest400
(toJSON (toError . filter keepPublicFailure $ e))
Right a -> pure (mkResp_ a)
where
printErrors = T.unlines . map (toFailureMessage . printe_ . extractFailure)
toError :: [Failure e] -> FailureResponse em
toError [] =
FailureResponse
{failureResponseError = "unknown error occurred"
,failureResponseAllErrors = []
}
toError es =
FailureResponse
{failureResponseError = printErrors es
,failureResponseAllErrors = map (printe_ . extractFailure) es
}
keepPublicFailure (FailurePrivate _) = False
keepPublicFailure (FailurePublic _) = True And -- | Version of "constraintViolationToFailure" explicitly generating a list of failures
constraintViolationToFailure'
:: (MonadBaseControl IO m, MonadCatch m, MonadError [Failure e] m)
=> (ed -> [Failure e]) -- ^ Convert the transaction abort reason e.g. "FailurePublic . asText" or "neverAborts"
-> (ConstraintViolation -> [Failure e]) -- ^ Convert the constraint violation e.g. "FailurePublic . tshow"
-> m (Either ed a) -- ^ The transaction that could fail or abort
-> m a
constraintViolationToFailure' abortToFailure constraintToFailure m =
catchJust
constraintViolation
(first abortToFailure <$> m)
(pure . Left . constraintToFailure) >>=
either throwError pure This is used as: postLocationsR :: Foo -> Handler Value
postLocationsR _ =
handleFailure "postLocationsR" asText (const $ toJSON Res.Success) $
do ReqM.NewLocation {..} <- requireJsonBodyFail
constraintViolationToFailure neverAborts (FailurePublic . tshow) $ runSerializableTransaction $
void $
insert ... Note that Are you on the fence due to dependency concerns, or correctness concerns? |
I should note that we're in this situation as we need to update our package set for other reasons, but with 300+ modules doing a large refactor at the same time is unfortunately more work than we have time for at the moment. I appreciate that's not a great reason to add things to an upstream library (your inconvenience for our convenience), but I do think these instances are popular enough to be useful. |
Definitely correctness, |
Is |
Yes, it's inherent to |
Is there anything I can read? I watched your "Everything You Don't Want To Know About Monad Transformer State" video yesterday (great talk!), but the story has changed since then. There is now afaik no discarding in generalBracket for ExceptT or StateT, so if there is still something problematic it'd be great to understand what that is. |
There is definitely discarding in try $ put newVal >> throwIO someExcVal There's no change to the definition of |
Oh right, though I agree with what is happening with Is there any other one liner that you think might be convincing? Let's leave out concurrency of this matter, and focus just on the space provided by Sorry this is slightly off topic, and I know you probably feel like you've been over this a thousand times! I appreciate the discussion. |
I doubt there's any one-liner I can provide that will be convincing. There's legitimate disagreement in the community about this topic. But I've seen enough broken code out there by people making assumptions about how code may behave that I stand strongly by this one. The basic idea is that, in many cases, people view a value of type I know some people are unconvinced by me on this, which is fine. But I remain strongly convinced that
|
Ok, so you're on the fence here because you obviously have a ton of downstream users, and you can't really control what they do, and you'd rather err on the side of caution. That's fair enough. I'm probably in the disagree camp, but this has given me a lot of food for thought, and I'm less certain in my own view point now. I'll think on this. Back to the topic at hand though, is there a disadvantage with having the instances in this library? Orphans are a mess, and I think if we were going to do that, I'd rather we just say "define the instances yourself" and consider this exceptional (ha!). All your exception handling is done with |
That's where I'm really on the fence. It doesn't make any existing code worse to add the instances. What it does do is make it easier to follow anti-patterns. Using the FWIW: prior art is here (commercialhaskell/rio#38), where we ended up with a rio-orphans library. |
I agree that Maybe let's keep this open and see if others have thoughts? Happy to experiment with orphans in my own code and see how that works out. |
Thinking about this a little more, the reason we end up with newtype Thrown a = Thrown { unThrown :: a }
instance Show ( Thrown a ) where
show _ = "<Thrown>"
instance ( Typeable a ) => Exception ( Thrown a )
newtype MapErrorT e m a = MapErrorT { unMapErrorT :: m a }
deriving ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadLogger )
instance MonadUnliftIO m => MonadUnliftIO (MapErrorT e m) where
askUnliftIO = do
UnliftIO f <- lift askUnliftIO
return (UnliftIO (f . unMapErrorT))
instance MonadTrans ( MapErrorT e ) where
lift = MapErrorT
instance MonadReader r m => MonadReader r ( MapErrorT e m ) where
ask = lift ask
local f (MapErrorT m) =
MapErrorT (local f m)
instance MonadLog msg m => MonadLog msg (MapErrorT e m) where
instance ( Typeable e, MonadCatch m ) => MonadError e (MapErrorT e m) where
throwError =
MapErrorT . throwM . Thrown
catchError m f =
MapErrorT ( try ( unMapErrorT m ) ) >>= either ( f . unThrown ) return
mapError :: ( MonadCatch m, MonadError e' m, Typeable e ) => (e -> e') -> MapErrorT e m a -> m a
mapError f = try . unMapErrorT >=> either ( throwError . f . unThrown ) return With this, I have the ability to eliminate a What's even cooler is this still generalises to pure code! Just drop in So, with the above I do still need Sorry that's a bit rambly, maybe something there makes sense :) |
This is a good discussion. Just my 2 cents, but IMO For example, servant has
The nice thing about having |
Hi! I'm hitting this today with some work code: we have some There is no Since there's no |
I think the issues of allowing/promoting
For example, consider instance MonadThrow App where
throwM = throwWithCallStack Or, consider a special exception wrapper type that indicates "this came from my code": instance MonadThrow App where
throwM = throwWithCallStack . wrapAppException Or, consider how you can define instance MonadCatch App where
catch = UnliftIO.catch The above case would also apply to using |
I know that Yesod now prefers
unliftio
, but there are valid instances that can be written forMonadCatch
andMonadMask
(I believe this follows fromHandlerFor
being isomorphic toReaderT
). Would you be open to having those instances?The text was updated successfully, but these errors were encountered: