Skip to content

Instantly share code, notes, and snippets.

@cscalfani
Last active September 2, 2020 23:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cscalfani/0bb0b00316d81d9ef3035c10b3772595 to your computer and use it in GitHub Desktop.
Save cscalfani/0bb0b00316d81d9ef3035c10b3772595 to your computer and use it in GitHub Desktop.

Monad Transformer Problem

The following problem is full demonstrated in this Gist in a file called Main.purs. You can try it out by using at Monad Transformer Problem. You'll have to open the Console log in the browser to see the results.

Using catchError in a Monad Stack where ExceptT is in the middle of the Stack, will cause everything above ExceptT in that Stack to be lost and everything below the ExceptT to be kept.

So imagine the following Stack:

+-------------------+
|      WriterT      | (is above ExceptT and NOT lost)
+-------------------+
|      ExceptT      |
+-------------------+
|      StateT       | (is below ExceptT and is lost)
+-------------------+

Now the following code is the code we want to try to run using catchError:

validate :: Int -> AppM
validate n = do
  s <- get
  let x = spy "s in validate" s
  log "HEY!!!!!!!!!!!!!!!!!!!" -- This will NOT be lost on the error case
  put 10 -- This will be lost on the error case but kept on the success case
  when (n == 0) $ void $ throwError "WE CANNOT HAVE A 0 STATE!"

And we're going to call it in the following way:

catchError (validate n) (\err -> do
  s <- get
  let x = spy "s in error handler" s
  log $ "We encountered an error: (" <> err <> ")"
  put 100
)

When an error occurs in validate, the changes to the log made by WriterT will be kept, but the change to the state made by StateT will not be.

This behavior is unexpected and subject to change when the Stack Order changes.

The Expected Loss

While catchError is an unexpected loss, the loss encountered during a run of a Monad Stack will be apparent in it's return Type.

The return Type for the above Monad Stack is:

Tuple (Either String (Tuple Unit Int)) String
  • e is the error Type
  • a is the Pure Computation Type
  • s is the state Type
  • w is the log Type

Notice how we lose state when we have an error but not the log. This is the same behavior as catchError's attempted computation. i.e. validate n in our above example.

module Main where
import Prelude
import Control.Monad.Except.Trans (ExceptT, runExceptT, throwError, catchError)
import Control.Monad.State.Trans (StateT, runStateT, get, put)
import Control.Monad.Writer.Trans (class MonadTell, WriterT, runWriterT, tell)
import Data.Either (Either)
import Data.Tuple (Tuple)
import Effect.Console as Console
import Effect (Effect)
import Debug.Trace (spy)
type AppM = StateT Int (ExceptT String (WriterT String Effect)) Unit
runApp :: Int -> AppM -> Effect (Tuple (Either String (Tuple Unit Int)) String)
runApp s = runWriterT <<< runExceptT <<< flip runStateT s
log :: ∀ m. MonadTell String m => String -> m Unit
log s = tell $ s <> "\n"
validate :: Int -> AppM
validate n = do
s <- get
let x = spy "s in validate" s
log "HEY!!!!!!!!!!!!!!!!!!!" -- This will NOT be lost on the error case
put 10 -- This will be lost on the error case but kept on the success case
when (n == 0) $ void $ throwError "WE CANNOT HAVE A 0 STATE!"
app :: AppM
app = do
log "Starting App..."
n <- get
catchError (validate n) (\err -> do
s <- get
let x = spy "s in error handler" s
log $ "We encountered an error: (" <> err <> ")"
put 100
)
s <- get
let x = spy "s in app" s
put $ s + 1
log "Incremented State"
pure unit
main :: Effect Unit
main = do
result1 <- runApp 0 app
Console.log $ show result1
result2 <- runApp 99 app
Console.log $ show result2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment