How are Type-Errors handled in Haskell's Either Monad?

Viewed 871

I am currently having problems understanding the Either-Monad in Haskell. Either is an instance of the Monad data type. Therefore I can use the Either within a Do block like this ...

type MyAccount = Integer

transaction :: Integer -> MyAccount -> Either String MyAccount
transaction value account | value + account < 0 = Left "Error: insufficient funds!" 
                          | otherwise = return $ account + value

Test1 :: Either String MyAccount
Test1 = do 
          account_state <- return 1000                       
          r1 <- transaction (-200) account_state             
          r2 <- transaction (-1000) r1                    
          return r2                                       

This is a working piece of Code: account_state, r1 and r2 are of type Integer. Therefore the function transaction works properly. However..

Test2 :: Either String MyAccount
Test2 = do 
          account_state <- return 1000
          r1 <- transaction (-2000) account_state
          r2 <- transaction (-1000) r1
          return r2

In Test2 I have change the transaction value from -200 to -2000. Therefore r1 should be the String "Error: insufficient funds!". Since transaction does not work with Strings, there should be a type error. But there is not ... What is Haskell doing here ?

transaction (-1000) ("Error: insufficient funds!") should not work.

3 Answers

...but r1 is a String

It's not. r1 is Integer, and it's always an integer.

This is how the Either monad works:

r1 <- transaction (-2000) account_state

When you run this code, the right-hand side transaction ... is a monadic action, with the type Either String Integer. However, the monadic bind <- assigns the result of the monadic action, which is always Integer, to r1.

If the monadic action is actually Left, then it is not possible to assign a value to r1, and in fact it won't happen... the function will return immediately instead.

You can translate the code to code which does not use monads (or at least does not use the Monad type in Haskell):

test2 =
    let account_state = 1000
    in case transaction (-2000) account_state of
         Left err -> Left err # Returns immediately
         Right r1 -> transaction (-1000) r1

I hope that the behavior of the function is a bit more obvious when we strip away the monads.

Some Notes

It's weird to see,

x <- return y

You can replace it with,

let x = y

which is simpler and easier to understand.

It's also unnecessary to write:

x <- y
return x

Instead, just write:

y

Welcome to the joy of monads in Haskell!

First of all, a lightly edited version of your code.

type MyAccount = Integer

transaction :: Integer -> MyAccount -> Either String MyAccount
transaction value account | value + account < 0 = Left "Error: insufficient funds!" 
                          | otherwise = return $ account + value

Test1 :: Either String MyAccount
Test1 = do 
          let account_state = 1000                       
          r1 <- transaction (-200) account_state             
          transaction (-1000) r1

Test2 :: Either String MyAccount
Test2 = do 
          let account_state = 1000
          r1 <- transaction (-2000) account_state
          transaction (-1000) r1

Either is an 'error monad'. it works as follows:

Each action is evaluated.

If it yields a Right (conventionally non-error value), that value is unwrapped and the following action is evaluated.

If the final action yields a Right, this is returned from the do block as a Right.

If any action evaluates to a Left value, evaluation does not continue to the next action, but returns from the do block immediately with the Left value.

Therefore, in your case, r1 will only receive a value, and subsequent actions which can read r1 will only be evaluated, if the action that assigns to r1 yields a Right. r1 will always be an Integer.

Other error monads, such as Maybe, work similarly.

Thanks you to Dietrich Epp and Silver Rampart. Both of your Answers are a perfect explanation for the Problem. Here is just an addition

do 
      account_state <- return 1000
      r1 <- transaction (-2000) account_state
      r2 <- transaction (-1000) r1
      return r2

Can be rewritten to :

do 
    (return 1000) >>= (transaction (-2000)) >>= (transaction (-1000))

where (>>=) is defined as

instance Monad (Either e) where
    return = Right
    Right m >>= k = k m
    Left e  >>= _ = Left e

Therefore the Left e >>= _ = Left e makes Haskell ignore everything connected with the (>>=) operator, if the first result is Left.

Related