Although I like @jozefgs solution for its brevity, I dislike that it uses exitWith
. Sure, it works fine for such a small example as this, but in general I think it's a bad idea to terminate the program "prematurely" like that. It's indeed a property I dislike of the Python version as well. In any case, my solution follows, and I'll explain a few parts of it below.
import System.Environment (getProgName, getArgs)
import Text.Read (readMaybe)
doSomethingInteresting :: Int -> Int -> IO ()
doSomethingInteresting a b = print (a + b)
readWith :: Read a => (String -> e) -> String -> Either e a
readWith err str = maybe (Left $ err str) return $ readMaybe str
main = do
progName <- getProgName
args <- getArgs
either putStrLn id $ do
(a,b) <- case args of
[a,b] -> return (a,b)
_ -> Left ("Must run program like `" ++ progName ++ " NUM_A NUM_B`")
num_a <- readWith (\s -> "Could not parse NUM_A \"" ++ s ++ "\" as integer") a
num_b <- readWith (\s -> "Could not parse NUM_B \"" ++ s ++ "\" as integer") b
return $ doSomethingInteresting num_a num_b
I know there's a pattern matching in this, but that is really the cleanest way to express what I want. If the list has two elements, I want them out of it. If it doesn't, I want an error message. There is no neater way of expressing that.
But there are a few other things going on here that I want to highlight.
Single exit point
First, which I've already mentioned, the program does not have several "exit points", i.e. it doesn't terminate the program in the middle of something. Imagine if the main
function was not the main function but somewhere deeper down, and you had some files open or whatnot in a function above. You would want to go out the proper way to close the files and so on. That's why I choose to not terminate the program, but rather let it run its course and just don't do the calculations if it doesn't have the number.
Just terminating the program is rarely a good idea. Often, you have things you want to save, files you want to close, state you want to restore and so on.
Either
annotates errors
It might not be very evident for a beginner, but what readWith
basically does is try to read
something, and if it succeeds, it returns Right <read value>
. If it fails, it will return Left <error message>
, where the error message can depend on what string it tried to read.
So, in this program,
λ> readWith errorMessage "15"
Right 15
while
λ> readWith errorMessage "crocodile"
Left "Cannot read \"crocodile\" as an integer, dummy!"
The Either
type is great when you want to transport a value but there might occur an error along the way, and you want to keep the error message around until you know what to do about the error. The community consensus is that Right
should indicate the "correct" value and Left
should indicate that some error occurred.
Either
values are a sort of more controlled (read: better) exception system.
do
syntax is your friend
The do
syntax is very handy for handling errors. As soon as any computation results in a Left
value, Haskell will break out of the do
block with the error message carried by the Left
. This means that the entire inner do
block will in this case either result in something similar to
Right (print 41)
or something like
Left "Could not parse NUM_A \"crocodile\" as integer"
The either
function then makes sure to either print the error message or just return the IO
action that is the Right
value. It might be weird coming from Python that you can store a print
action as a value, but that's normal in Haskell. We say that I/O actions are first class citizens in Haskell. In other words, we can pass them around and store them in data structures, and then we decide ourselves when we want them executed.