How to implement early exit / return in Haskell?
Asked Answered
N

3

11

I am porting a Java application to Haskell. The main method of the Java application follows the pattern:

public static void main(String [] args)
{
  if (args.length == 0)
  {
    System.out.println("Invalid number of arguments.");

    System.exit(1);
  }

  SomeDataType d = getData(arg[0]);
  if (!dataOk(d))
  {
    System.out.println("Could not read input data.");

    System.exit(1);
  }

  SomeDataType r = processData(d);
  if (!resultOk(r))
  {
    System.out.println("Processing failed.");

    System.exit(1);
  }

  ...
}

So I have different steps and after each step I can either exit with an error code, or continue to the following step.

My attempt at porting this to Haskell goes as follows:

main :: IO ()
main = do
         a <- getArgs
         if (null args)
           then do
                   putStrLn "Invalid number of arguments."
                   exitWith (ExitFailure 1)
           else do
                   -- The rest of the main function goes here.

With this solution, I will have lots of nested if-then-else (one for each exit point of the original Java code).

Is there a more elegant / idiomatic way of implementing this pattern in Haskell? In general, what is a Haskell idiomatic way to implement an early exit / return as used in an imperative language like Java?

Neology answered 20/12, 2012 at 10:55 Comment(5)
Please read the example of Walking the Line from learnyouahaskell.com/a-fistful-of-monads page. It gives example of Monad using Maybe datatype. Once the result of any expression is Nothing, the result of all following expressions are Nothing, as good as you have exited at the failure point.Awning
@ManojR - Maybe isn't really suitable here since you also want the reason for the failure.Stoush
The classic approach is splitting the process into a "trusting" processing function (that assumes parameters are correct) and a "paranoid" sanity check function (that only checks if parameters are correct)...Disfrock
Please please please do not check for an empty list using length. Use null if you actually need to simply check, but prefer pattern matching, because that combines the check with giving a name to the list element(s) so you can use them.Idolize
@C. A. McCann: Thanks. I normally use null. length slipped in from the Java original and I did not care too much about it because I expect the list of arguments to be rather short. But, yes, null is definitely the way to go.Neology
I
7

A slightly more sensible approach in Haskell that uses the same sort of conditional logic you tried might look like this:

fallOverAndDie :: String -> IO a
fallOverAndDie err = do putStrLn err
                        exitWith (ExitFailure 1)

main :: IO ()
main = do a <- getArgs
          case a of
              [d] | dataOk d  -> doStuff $ processData d
                  | otherwise -> fallOverAndDie "Could not read input data."
              _ -> fallOverAndDie "Invalid number of arguments."


processData r 
    | not (resultOk r) = fallOverAndDie "Processing failed."
    | otherwise        = do -- and so on...

In this particular case, given that exitWith terminates the program anyway, we could also dispense with the nested conditionals entirely:

main :: IO ()
main = do a <- getArgs
          d <- case a of
                   [x] -> return x
                   _   -> fallOverAndDie "Invalid number of arguments."
          when (not $ dataOk d) $ fallOverAndDie "Could not read input data."
          let r = processData d
          when (not $ resultOk r) $ fallOverAndDie "Processing failed."

Using the same fallOverAndDie as before. This is a much more direct translation of the original Java.

In the general case, the Monad instance for Either lets you write something very similar to the latter example above in pure code. Starting from this instead:

fallOverAndDie :: String -> Either String a
fallOverAndDie = Left

notMain x = do a <- getArgsSomehow x
               d <- case a of
                        -- etc. etc.

...the rest of the code is unchanged from my second example. You can of course use something other than just String as well; to more faithfully recreate the IO version, you could use Either (String, ExitCode) instead.

Additionally, this use of Either is not limited to error handling--if you have some complicated calculation returning a Double, using Either Double Double and the same monadic style as above, you can use Left to bail out early with a return value, then wrap the function using something like either id id to collapse the two outcomes and get a single Double.

Idolize answered 20/12, 2012 at 17:58 Comment(0)
I
5

One way is to use the ErrorT monad transformer. With it, you can treat it like a regular monad, return, bind, all that good stuff, but you also get this function, throwError. This causes you to skip the following calculations either till you reach the end of the monadic computation, or when you call catchError. This is for error handling though, it's not meant to be for arbitrarily exiting a function in Haskell. I suggested it because it seems like that's what you're doing.

A quick example:

import Control.Monad.Error
import System.Environment

data IOErr = InvalidArgs String | GenErr String deriving (Show)
instance Error IOErr where
  strMsg = GenErr --Called when fail is called
  noMsg  = GenErr "Error!"
type IOThrowsError = ErrorT IOErr IO

process :: IOThrowsError [String]
process = do
  a <- liftIO getArgs
  if length a == 0
  then throwError $ InvalidArgs "Expected Arguments, received none"
  else return a

main = do 
  result <- runErrorT errableCode
  case result of
     Right a -> putStrLn $ show a
     Left  e -> putStrLn $ show e
  where errableCode = do
    a <- process
    useArgs a

now if process threw an error, useArgs wouldn't be executed.

Interstitial answered 20/12, 2012 at 11:23 Comment(0)
A
0

This is what I have came up with

data ExtendedMaybe a = Just a | GenErr String 

isWrongArgs :: [string] -> ExtendedMaybe [string]
isWrongArgs p = if (length p == 0)
then GenErr "Invalid number of arguments"
else p

getData :: ExtendedMaybe [string] -> ExtendedMaybe sometype 
getData GenErr = GenErr
getData [string] = if anything wrong return GenErr "could not read input data"

processdata :: ExtendedMaybe sometype -> ExtendedMaybe sometype
processdata GenErr = GenErr 

main = do 
    a <- getArgs
    d <- isWrongArgs a
    r <- getData d
    f <- processdata r

Roughly the idea is you have a datatype like Maybe a, only instead of Nothing you have GenErr String, which you define in every function which process data. If the input data type is GenErr simply return that. Otherwise check the error in the data and return GenErr with appropriate string. This may not be the perfect way, but still one way. This do not exit at the exact point of error, but guarantee that not much is happening after error occurred.

Awning answered 20/12, 2012 at 14:4 Comment(1)
Your ExtendedMaybe type is basically the same as Either StringStoush

© 2022 - 2024 — McMap. All rights reserved.