This is a simple question with a complex answer I presume.
A very common programming problem is a function that returns something, or fails precondition checks. In Java I would use some assert function that throws IllegalArgumentException at the beginning of the method like so:
{
//method body
Assert.isNotNull(foo);
Assert.hasText(bar)
return magic(foo, bar);
}
What I like about this is that it is a oneliner for each precondition. What I don't like about this is that an exception is thrown (because exception ~ goto).
In Scala I've worked with Either, which was a bit clunky, but better than throwing exceptions.
Someone suggested to me:
putStone stone originalBoard = case attemptedSuicide of
True -> Nothing
False -> Just boardAfterMove
where {
attemptedSuicide = undefined
boardAfterMove = undefined
}
What I don't like is that the emphasis is put on the True and the False, which mean nothing by themselves; the attemptedSuicide precondition is hiding in between syntax, so not clearly related to the Nothing AND the actual implementation of putStone (boardAfterMove) is not clearly the core logic. To boot it doesn't compile, but I'm sure that that doesn't undermine the validity of my question.
What is are the ways precondition checking can be done cleanly in Haskell?
In Haskell, working with Maybe and Either is a bit slicker than Scala, so perhaps you might reconsider that approach. If you don't mind, I will use your first example to show this.
First off, you usually wouldn't test for null. Instead, you would just compute the property you were actually interested in, using Maybe to handle failure. For example, if what you actually wanted was the head of the list, you could just write this function:
-- Or you can just import this function from the `safe` package
headMay :: [a] -> Maybe a
headMay as = case as of
[] -> Nothing
a:_ -> Just a
For something that is purely validation, like hasText, then you can use guard, which works for any MonadPlus like Maybe:
guard :: (MonadPlus m) => Bool -> m ()
guard precondition = if precondition then return () else mzero
When you specialize guard to the Maybe monad then return becomes Just and mzero becomes Nothing:
guard precondition = if precondition then Just () else Nothing
Now, suppose that we have the following types:
foo :: [A]
bar :: SomeForm
hasText :: SomeForm -> Bool
magic :: A -> SomeForm -> B
We can handle errors for both foo and bar and extract the values safely for the magic function using do notation for the Maybe monad:
example :: Maybe B
example = do
a <- headMay foo
guard (hasText bar)
return (magic a bar)
If you're familiar with Scala, do notation is like Scala's for comprehensions. The above code desugars to:
example =
headMay foo >>= \a ->
guard (hasText bar) >>= \_ ->
return (magic a bar)
In the Maybe monad, (>>=) and return have the following definitions:
m >>= f = case m of
Nothing -> Nothing
Just a -> f a
return = Just
... so the above code is just short-hand for:
example = case (headMay foo) of
Nothing -> Nothing
Just a -> case (if (hasText bar) then Just () else Nothing) of
Nothing -> Nothing
Just () -> Just (magic a bar)
... and you can simplify that to:
example = case (headMay foo) of
Nothing -> Nothing
Just a -> if (hasText bar) then Just (magic a bar) else Nothing
... which is what you might have written by hand without do or guard.
You have two options:
Option 1. is of course preferred, but it's not always possible. For example, you can't say in Haskell's type systems that one argument is greater than other one, etc. But still you can express a lot, usually much more than in other languages. There are also languages that use so called dependent types and which allow you to express any condition in their type system. But they're mostly experimental or research work. If you're interested, I suggest you to read book Certified Programming with Dependent Types by Adam Chlipala.
Doing run-time checks is easier and it's what programmers are more used to. In Scala you can use require in your methods and recover from the corresponding exception. In Haskell this is trickier. Exceptions (caused by failing pattern guards, or issued by calling error or undefined) are by their nature IO based, so only IO code can catch them.
If you suspect that your code can fail for some reasons, it's better to use Maybe or Either to signal failures to the caller. The drawback is that this will make the code more complex and less readable.
One solution is to embed your computations into an error handling/reporting monad, such as MonadError. Then you can report errors cleanly and catch them somewhere at a higher level. And if you're already using a monad for your computations, you can just wrap your monad into EitherT transformer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With