I've been definiing custom effects as described in Effectful.Dispatch.Dynamic
My application has a large runner which adds all the effects, then in my functions, I rely only on the effect. If I understand correctly, this is all 100% by-the-book: My app has a bunch of capabilities, and individual functions only depend on the effects they need.
data FileSystem :: Effect where
ReadFile :: FilePath -> FileSystem m String
runFileSystemIO
:: (IOE :> es, Error FsError :> es)
runApp :: Eff (SomethingElse : FileSystem : Error FsError : es) a -> Eff es a
runApp = runErrorNoCallstackWith @FsError catchLogAndThrowError . runFileSystem . runSomethingElse
example :: FileSystem :> es => Eff es ()
example = doSomething <<= send $ ReadFile "somefile.txt
However, sometimes I need to handle errors in a handler, at the callsite where the effect is used. Perhaps I want to retry reading the file if it fails, or do something else. This doesn't seem possible. runError doesn't work, because the error effect is used inside the runner for FileSystem.
-- this does not work
example :: FileSystem :> es => Eff es ()
example = do
res <- runErrorNoCallstack @FsError $ send $ ReadFile "somefile.txt
case res of
Left err -> putStrLn "do something custom with the error"
Right contents -> putStrLn "handle file normally"
The only way I can think of to get this to work would be to re-run the file-system effect:
-- `es` contains FileSystem already
example :: (IOE :> es) => Eff es ()
example = do
res <- runErrorNoCallstack @FsError $ runFileSystem $ send $ ReadFile "somefile.txt
case res of
Left err -> _
Right contents -> _
But I have to pass IOE around everywhere now! I lose all the benefits of isolating individual effects. My handler depends on all the global effects, like IOE. It gets worse if the effect in question requires several pieces of global app config, etc.
Standard exceptions handle this use-case quite gracefully: I can catch errors globally in some app runner, but if I catch them locally, I can handle them. But an effect that can throw Exceptions doesn't immediately tell you which errors can occur, or force you to handle them.
What is the right way to do this?
I've been definiing custom effects as described in Effectful.Dispatch.Dynamic
My application has a large runner which adds all the effects, then in my functions, I rely only on the effect. If I understand correctly, this is all 100% by-the-book: My app has a bunch of capabilities, and individual functions only depend on the effects they need.
However, sometimes I need to handle errors in a handler, at the callsite where the effect is used. Perhaps I want to retry reading the file if it fails, or do something else. This doesn't seem possible.
runErrordoesn't work, because the error effect is used inside the runner forFileSystem.The only way I can think of to get this to work would be to re-run the file-system effect:
But I have to pass
IOEaround everywhere now! I lose all the benefits of isolating individual effects. My handler depends on all the global effects, like IOE. It gets worse if the effect in question requires several pieces of global app config, etc.Standard exceptions handle this use-case quite gracefully: I can catch errors globally in some app runner, but if I catch them locally, I can handle them. But an effect that can throw Exceptions doesn't immediately tell you which errors can occur, or force you to handle them.
What is the right way to do this?