问题
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:
- Encode your preconditions in your types so that they're checked at compile-time.
- 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