fraxl icon indicating copy to clipboard operation
fraxl copied to clipboard

Catching errors thrown in the fetch functions

Open themoritz opened this issue 6 years ago • 5 comments

I'm trying to understand why using the catchError of Fraxl doesn't catch errors thrown by fetch functions. For example, the following code prints Left "Error" instead of Right 1:

data ErrorSource a where
  ThrowError :: ErrorSource a

fetchErrorSource :: Fetch ErrorSource (Either String)
fetchErrorSource = traverseASeq $ \ThrowError -> pure $ throwError "Error"

myThrowError :: Fraxl '[ErrorSource] (Either String) Int
myThrowError = dataFetch ThrowError `catchError` \_ -> pure 1

main :: IO ()
main =
  print $ runFraxl (fetchErrorSource |:| fetchNil) myThrowError

themoritz avatar Mar 12 '18 18:03 themoritz

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?

themoritz avatar Mar 16 '18 09:03 themoritz

@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?

MichaelXavier avatar Aug 26 '18 20:08 MichaelXavier

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:

  1. fmap f (return x) = return (f x) (same for liftM)
  2. 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.

ElvishJerricco avatar Nov 27 '18 00:11 ElvishJerricco

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.

ElvishJerricco avatar Nov 27 '18 01:11 ElvishJerricco

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.

ElvishJerricco avatar Nov 27 '18 01:11 ElvishJerricco