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