What are the options for precondition checking in Haskell

天大地大妈咪最大 提交于 2019-12-23 15:28:55

问题


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?


回答1:


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.




回答2:


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.




回答3:


You could handle all preconditions in an pattern guard at the beginning:

putStone stone originalBoard | attemptedSuicide = Nothing
  where attemptedSuicide = ...

putStone stone originalBoard = Just ...



回答4:


I'm going to take a broader perspective on this.

In Haskell we generally distinguish between three types of functions:

  • Total functions are guaranteed to give the right result for all arguments. In your terms, the preconditions are encoded in the types. This is the best kind of function. Other languages make it difficult to write this kind of function, for instance because you can't eliminate null references in the type system.

  • Partial functions are guaranteed to either give the right result or to throw an exception. "head" and "tail" are partial functions. In this case you document the precondition in the Haddock comments. You don't need to worry about testing the precondition because if you violate it an exception will be thrown anyway (although sometimes you put in a redundant test in order to give the developer a useful exception message).

  • Unsafe functions can produce corrupt results. For instance the Data.Set module includes a function "fromAscList" which assumes that its argument is already sorted into ascending order. If you violate this precondition then you get a corrupt Set rather than an exception. Unsafe functions should be clearly marked as such in the Haddock comments. Obviously you can always turn an unsafe function into a partial function by testing the precondition, but in many cases the whole point of the unsafe function is that this would be too expensive for some clients, so you offer them the unsafe function with appropriate warnings.

Because Haskell values are immutable you don't generally have difficulty in enforcing invariants. Suppose that in Java I have a class Foo that owns a Bar, and the Foo has some extra data that must be consistent with the contents of the Bar. If some other part of the code modifies the Bar without updating the Foo then the invariants are violated in a way that the author of Foo cannot prevent. Haskell does not have this problem. Hence you can create complicated structures with internal invariants enforced by their creator functions without having to worry about some other piece of code violating those invariants. Again, Data.Set provides an example of this kind of code; the total functions in Data.Set don't need to worry about checking the validity of the Set objects because the only functions that can create a Set are in the same module, and hence can be trusted to get it right.

One compromise between partial and unsafe would be the use of "Control.Exception.assert", which GHC treats as a special case, giving useful error messages for assertion failures, but disabling the checks when optimisation is turned on. See the GHC docs for details.



来源:https://stackoverflow.com/questions/17880137/what-are-the-options-for-precondition-checking-in-haskell

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!