What is a good way for a Haskell function to check a number of different conditions and return an error message on a failure?
In Python or similar language, it would be straightforward:
if failure_1:
return "test1 failed"
if failure_2:
return "test2 failed"
...
if failure_n:
return "testn failed"
do_computation
How do you do this without arbitrarily nested case/if statements in Haskell?
Edit: some of the test conditions may require IO which puts any test results in the IO monad. I believe this puts a kink in a number of solutions.
So, you’re stuck inside
IO, and you want to check a bunch of conditions without lots of nestedifs. I hope you’ll forgive me a digression onto more general problem solving in Haskell by way of answering.Consider in abstract how this needs to behave. Checking a condition has one of two outcomes:
Checking multiple conditions can be viewed recursively; each time it runs “the rest of the function” it hits the next condition, until reaching the final step which just returns the result. Now, as a first step to solving the problem, let’s break things apart using that structure–so basically, we want to turn a bunch of arbitrary conditions into pieces that we can compose together into a multi-conditional function. What can we conclude about the nature of these pieces?
1) Each piece can return one of two different types; an error message, or the result of the next step.
2) Each piece must decide whether to run the next step, so when combining steps we need to give it the function representing the next step as an argument.
3) Since each piece expects to be given the next step, to preserve uniform structure we need a way to convert the final, unconditional step into something that looks the same as a conditional step.
The first requirement obviously suggests we’ll want a type like
Either String afor our results. Now we need a combining function to fit the second requirement, and a wrapping function to fit the third. Additionally, when combining steps, we may want to have access to data from a previous step (say, validating two different inputs, then checking if they’re equal), so each step will need to take the previous step’s result as an argument.So, calling the type of each step
err aas a shorthand, what types might the other functions have?Well now, those type signatures look strangely familiar, don’t they?
This general strategy of “run a computation that can fail early with an error message” indeed lends itself to a monadic implementation; and in fact the mtl package already has one. More importantly for this case, it also has a monad transformer, which means that you can add the error monad structure onto another monad–such as
IO.So, we can just import the module, make a type synonym to wrap
IOup in a warm fuzzyErrorT, and away you go:The result of running
test, as you would expect, is eitherRight ()for success, orLeft Stringfor failure, where theStringis the appropriate message; and if anassertreturns failure, none of the following actions will be performed.For testing the result of
IOactions you may find it easiest to write a helper function similar toassertthat instead takes an argument ofIO Bool, or some other approach.Also note the use of
liftIOto convertIOactions into values inEIO, andrunErrorTto run anEIOaction and return theEither String avalue with the overall result. You can read up on monad transformers if you want more detail.