Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the options for precondition checking in Haskell

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?

like image 439
iwein Avatar asked Nov 30 '22 03:11

iwein


2 Answers

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.

like image 77
Gabriella Gonzalez Avatar answered Dec 05 '22 06:12

Gabriella Gonzalez


You have two options:

  1. Encode your preconditions in your types so that they're checked at compile-time.
  2. At run-time check that your preconditions hold so that your programs stops before doing something nasty and unexpected. Gabriel Gonzales shows this in detail his answer

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.

like image 42
Petr Avatar answered Dec 05 '22 04:12

Petr