How do you return early (with Right) from a function with return type Either?

I’m trying to write a function that takes some input x and returns an Either. This function tries to apply three functions f, g, h in that order to the input x, and returns the first successful (= Right) result. If everything fails, it returns Left. How do I do that?

Here’s a concrete example. These are our three functions f, g and h:

halveIfEight :: Int -> Either String Int
halveIfEight 8 = Right 4
halveIfEight x = Left "not eight"

halveIfEven :: Int -> Either String Int
halveIfEven x | x `mod` 2 == 0      = Right $ x `div` 2
              | otherwise           = Left "not even"
              
halveIfPositive :: Int -> Either String Int
halveIfPositive x | x > 0           = Right $ x `div `2
                  | otherwise       = Left "not positive"

In the code below, halve is the function in question:

halve :: Int -> Either String Int
halve x =
  case halveIfEight x of
    Right y -> Right y
    Left err1 ->
      case halveIfEven x of
        Right y -> Right y
        Left err2 ->
          case halveIfPositive x of
            Right y -> Right y
            Left err3 -> Left $ err1 ++ ", " ++ err2 ++ " and " ++ err3

main :: IO ()
main = do
  putStrLn $ show $ halve 8       --  Right 4
  putStrLn $ show $ halve 6       --  Right 3
  putStrLn $ show $ halve (-4)    --  Right (-2)
  putStrLn $ show $ halve (-3)    --  Left "not eight, not even and not positive"

halve behaves correctly, but I feel like there’s too much nesting. And my implementation doesn’t generalize to the case when I need to try indefinitely many sub-operations. Is there a cleaner/more idiomatic way to achieve what I want here?

I think you can just do simple recursion with an accumulator here:

go acc [] = Left (reverse acc)
go acc (Right y : _) = Right y
go acc (Left e  : rs) = go (e:acc) rs

And in the end you either pretty print your errors or return the right side:

bimap pretty id (go [] results)

This effectively says “if Left apply the first function otherwise apply the first”.

In full this would look something like:


import Data.Functor
import Data.List (intercalate)

halve :: Int -> Either String Int
halve x = bimap pretty id (go [] results)
  where
    fns = [halveIfEight, halveIfEven, halveIfPositive]
    results = zipWith ($) fns (cycle [x])

    go acc [] = Left (reverse acc)
    go acc (Right y : _) = Right y
    go acc (Left e  : rs) = go (e:acc) rs

    pretty errs =
      case errs of
        []  -> ""
        [e] -> e
        _   -> intercalate ", " (init errs) <> " and " <> last errs

I like this idea! I tried something similar with foldl:

halve :: Int -> Either String Int
halve x =
  let
    fs = [ halveIfEight, halveIfEven, halveIfPositive ]
    
    folder (Just (Right x)) _f = Just $ Right x
    folder _res f = Just $ f x
  in
    fromJust $ foldl folder Nothing fs

I think you can use MonadError instance of Either - something like:

x a = halfIfEight a `catchError` \e1 -> halfIfEven a `catchError` \e2 -> halveIfPositive a `catchError` \e3 -> throwError (e1 <> e2 <> e3)
1 Like

The more idiomatic way would be to use the MonadError instance of Either

halve :: Int -> Either String Int
halve x =
  halveIfEight x    `catchError` \ err1 ->
  halveIfEven x     `catchError` \ err2 ->
  halveIfPositive x `catchError` \ err3 ->
  Left $ err1 ++ ", " ++ err2 ++ " and " ++ err3

Probably also worth investigating the Monad instance of Either.

Consider swapping the roles of Left and Right. then you could use mapM on a list of your functions.

7 Likes

The (Monad m, Monoid e) => Alternative (ExceptT e m) instance of the Except type from transformers does what you want. Either the first success, or the collection of errors. You need to first put the error in each Left into a Monoid, though:

ghci> import Control.Monad.Trans.Except
ghci> import Control.Applicative
ghci> runExcept @[String] @Int $ except (Left ["foo"]) <|> except (Right 5) <|> except (Right 6)
Right 5
ghci> runExcept @[String] @Int $ except (Left ["foo"]) <|> except (Left ["bar"])
Left ["foo","bar"]

Either doesn’t have a similar Alternative instance, but it could be easily defined on a newtype. The existing Semigroup instance for Either does almost what you want, except that it only retains the last error, which seems less useful than retaining all the erros:

ghci> Left @[String] @Int ["foo"] <> Left ["bar"]
Left ["bar"]
ghci> Left @[String] @Int ["foo"] <> Right 5 <>  Left ["bar"]
Right 5
ghci> Left @[String] @Int ["foo"] <> Right 5 <> Right 6 <> Left ["bar"]
Right 5
2 Likes

It seems like you want exactly the swapped semantics of Either, no?

halveIfEight :: Int → Either Int String
halveIfEight 8 = Left 4
halveIfEight x = Right “not eight”

halveIfEven :: Int → Either Int String
halveIfEven x | x mod 2 == 0      = Left $ x div 2
              | otherwise           = Right “not even”

halveIfPositive :: Int → Either Int String
halveIfPositive x | x > 0           = Left $ x div 2
                  | otherwise       = Right “not positive”

halve :: Int → Either Int String
halve x = do
  err1 ← halveIfEight x
  err2 ← halveIfEven x
  err3 ← halveIfPositive x
  return (err1 ++ ", " ++ err2 ++ " and " ++ err3)

You could even use something like mapM or similar, as suggested by @augustss

halve :: Int -> Either Int String
halve x = intercalate ", "
      <$> sequenceA [halveIfEight x, halveIfEven x, halveIfPositive x]

I know that it might seem unnatural to put the “error” case in the Right constructor, but semantically here you’re just looking for short-circuiting on an answer, with accumulation in the error case, which is best matched by Either answer error (in my opinion).

3 Likes

There was a discussion about this on r/Haskell recently, about how Either should’ve been called Error etc. I think this is a great example of why Either Monad’s Left-Right asymmetry shouldn’t be conflated with error propagation, it does what it does and sometimes it happens to be convenient for error handling…

6 Likes

For an early return (short-circuit) on an initial segment of a list, the correct primitive is foldr rather than foldl. With a non-empty input foldr1 is sometimes a more natural choice:

λ> import Data.Bifunctor (first)

λ> erradd r e = first (((e ++ ", ") ++)) r
λ> vals = [Left "foo", Left "bar", Left "baz"]
λ> foldr1 (\x r -> either (erradd r) Right x) vals
Left "foo, bar, baz"
λ> vals = [Left "foo", Right 42, Left "baz"]
λ> foldr1 (\x r -> either (erradd r) Right x) vals
Right 42

Though in this case, I’d go with a list of error strings, rather than a concatenated error string, in whcih case foldr is more natural:

λ> erradd r e = first (e :) r
λ> op x r = either (erradd r) Right x
λ> valsr = [Left "foo", Right 42, Left "baz"]
λ> valsl = [Left "foo", Left "bar", Left "baz"]
λ> foldr op (Left []) valsr
Right 42
λ> foldr op (Left []) valsl
Left ["foo","bar","baz"]