Open themoritz opened 6 years ago
Ok, I dug a little bit into this and the problem is that, fundamentally, errors thrown during interpretation of the free monad cannot be caught by the lifted catchError
. This can be seen if write the same example using the simple FreeT
from the free package:
data ResourceF a
= GetInt (Int -> a)
deriving Functor
program :: FreeT ResourceF (Either String) Int
program = liftF (GetInt id) `catchError` const (pure 1)
result :: Either String Int
result = iterT fetch program -- = Left "error"
where fetch (GetInt next) = throwError "error"
The next
is never used, so the fetch function short circuits interpretation and there is no way for the user code to recover from it.
We can solve this by adding a Catch
node to the syntax tree, so that it becomes part of the DSL:
data ResourceF e a
= GetInt (Int -> a)
| Catch a (e -> a)
deriving Functor
getInt :: Monad m => FreeT (ResourceF e) m Int
getInt = liftF (GetInt id)
myCatch :: Monad m => FreeT (ResourceF e) m a -> (e -> FreeT (ResourceF e) m a) -> FreeT (ResourceF e) m a
myCatch m f = wrap (Catch m f)
program :: FreeT (ResourceF String) (Either String) Int
program = getInt `myCatch` \_ -> pure 1
result :: Either String Int
result = iterT fetch program -- = Right 1
where fetch (GetInt next) = throwError "error"
fetch (Catch next f) = catchError next f
Now to solve this in fraxl, the question is how do I define a resource that encodes a catch function?
@ElvishJerricco Any opinion on this? I just came across this library and saw some comments to the effect that you were considering moving some production systems to fraxl. Did that happen, and if so, what exception handling techniques emerged?
Still trying to wrap my head around this... If we use Control.Monad.Trans.Free
, and start inlining and reducing an expression, assuming these two laws:
fmap f (return x) = return (f x)
(same for liftM
)return x `catchError` f = return x
liftF x `catchError` f
-- Inline liftF
= (wrap . fmap return) x `catchError` f
-- Inline wrap
= (FreeT $ return $ Free $ fmap return x) `catchError` f
-- inline catchError @FreeT
= FreeT $ liftM (fmap (`catchError` f)) (return $ Free $ fmap return x) `catchError` (runFreeT . f)
-- liftM f (return x) = return (f x)
= FreeT $ return (fmap (`catchError` f) (Free $ fmap return x)) `catchError` (runFreeT . f)
-- return x `catchError` f = return x
= FreeT $ return (fmap (`catchError` f) (Free $ fmap return x))
-- fmap f (return x) = return (f x)
= FreeT $ return (Free $ fmap ((`catchError` f) . return) x)
-- return x `catchError` f = return x
= FreeT $ return (Free $ fmap return x)
We see that liftF x `catchError` f
will always drop the f
. This is pretty disappointing, and I'm not sure what can be done about it.
I think it makes sense though. The interpreter isn't throwing to the application code. It's throwing to the toplevel code that called the interpreter.
So I'd suggest this: Have your data source return Either
, and put ExceptT
below Fraxl
(i.e. Fraxl MyReq (ExceptT MyError m)
). Do not have the interpreter use throwError
if you intend for the error to be caught by the application. Instead, just have it return Left
to the application, and have the application use:
data MyReq a where
MightFail :: Args -> MyReq (Either MyError Res)
liftEither :: MonadError e m => Either e a -> m a
liftEither = either throwError return
mightFail :: (MonadFraxl MyReq m, MonadError MyError m) => Args -> m Res
mightFail args = liftEither =<< dataFetch (MightFail args)
Note that you cannot use ExceptT
over Fraxl
, because the (<*>)
instance for ExcetT
must run the first argument monadically to see whether it needs to short circuit on Left
before it can start the second argument. Therefore it cannot be concurrent. See here, and here.
I'm trying to understand why using the
catchError
ofFraxl
doesn't catch errors thrown by fetch functions. For example, the following code printsLeft "Error"
instead ofRight 1
: