What are the options for checking prerequisites in Haskell

This is a simple question with a complex answer that I guess.

A very common programming problem is a function that returns something or does not perform precondition checks. In Java, I would use some assert function that throws an IllegalArgumentException at the beginning of the method as follows:

 { //method body Assert.isNotNull(foo); Assert.hasText(bar) return magic(foo, bar); } 

What I like about this is that he is an oneliner for every precondition. What I don't like about this is that an exception is thrown (since exception ~ goto).

In Scala, I worked with Either, which was a bit awkward, 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 on Truths and lies, which in themselves do not mean anything; The attemptedSuicide precondition is hidden between the syntax, so it is clearly not related to Nothing, and the actual implementation of putStone (boardAfterMove) is clearly not the main logic. It does not compile for download, but I am sure that this does not undermine the legitimacy of my question.

What are the ways to verify prerequisites in Haskell clean?

+4
source share
4 answers

You have two options:

  • Code your preconditions in your types so that they are checked at compile time.
  • At run time, verify that your prerequisites are saved so that your programs stop before doing anything unpleasant and unexpected. Gabriel Gonzalez shows this in detail his answer

Option 1., of course, is preferable, but this is not always possible. For example, you cannot say on systems like Haskell that one argument is greater than the other, etc. But you can still express a lot, usually much more than in other languages. There are also languages ​​that use the so-called dependent types and which allow you to express any conditions in their type system. But they are mostly experimental or research. If you are interested, I suggest you read the book Certified Programming with Dependent Types from Adam Hlipala.

Performing checks at run time is simpler, and programmers are more familiar with it. In Scala, you can use require in your methods and recover from the corresponding exception. In Haskell, this is more complicated. Exceptions (caused by error protection or thrown when error or undefined called) are inherently based on IO , so only IO code can catch them.

If you suspect that for some reason your code may be out of order, it is better to use Maybe or Either to signal caller crashes. The downside is that this will make the code more complex and less readable.

One solution is to integrate your calculations into the error handling / reporting monad, such as MonadError . Then you can clearly report bugs and catch them somewhere at a higher level. And if you already use the monad for your calculations, you can just wrap your monad in EitherT .

+5
source

At Haskell, working with Maybe and Either slightly weaker than Scala, so maybe you can rethink this approach. If you do not mind, I will use your first example to show this.

First, you usually do not check for null. Instead, you would simply calculate the property you are interested in using Maybe to handle the failure. For example, if what you really wanted was the head of the list, you could simply 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 hasText , like hasText , you can use a guard that works for any MonadPlus like Maybe :

 guard :: (MonadPlus m) => Bool -> m () guard precondition = if precondition then return () else mzero 

When you specialize guard in the Maybe monad, then return becomes Just and mzero becomes Nothing :

 guard precondition = if precondition then Just () else Nothing 

Now suppose 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 safely retrieve values ​​for the magic function using the do notation for Maybe monad:

 example :: Maybe B example = do a <- headMay foo guard (hasText bar) return (magic a bar) 

If you are familiar with the notation of Scala, do , like Scala for understanding. The above code contains the following parameters:

 example = headMay foo >>= \a -> guard (hasText bar) >>= \_ -> return (magic a bar) 

In the monad Maybe (>>=) and return have the following definitions:

 m >>= f = case m of Nothing -> Nothing Just a -> fa return = Just 

... therefore the above code is just concise 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 this:

 example = case (headMay foo) of Nothing -> Nothing Just a -> if (hasText bar) then Just (magic a bar) else Nothing 

... this is something you could write manually without do or guard .

+7
source

First, you can handle all the prerequisites in protecting the template:

 putStone stone originalBoard | attemptedSuicide = Nothing where attemptedSuicide = ... putStone stone originalBoard = Just ... 
+5
source

I am going to consider a broader view of this.

In Haskell, we usually distinguish between three types of functions:

  • The full number of functions is guaranteed to give the correct result for all arguments. In your conditions, preconditions are encoded in types. This is the best kind of function. Other languages ​​make it difficult to write these kinds of functions, for example, because you cannot eliminate null references in a type system.

  • Partial functions are guaranteed to either give the correct result or throw an exception. "head" and "tail" are partial functions. In this case, you confirm the precondition in the comments on Haddock. You don’t have to worry about testing the precondition because if you break it, an exception will still be thrown (although sometimes you add a redundant test to give the developer a useful exception message).

  • Unsafe functions can lead to corrupted results. For example, the Data.Set module includes a function fromAscList, which assumes that its argument is already sorted in ascending order. If you violate this precondition, you will receive a damaged set, not an exception. Unsafe features should be clearly indicated as such in the comments on the Haddock. 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 it will be too expensive for some clients, so you offer them an unsafe function with the corresponding warnings.

Because Haskell values ​​are immutable, you usually have no difficulty in enforcing invariants. Suppose in Java I have the Foo class that owns the panel, and Foo has extra data that should match the contents of the Bar. If any other part of the code modifies Bar without updating Foo, then the invariants are violated in such a way that the author of Foo cannot prevent it. Haskell does not have this problem. Therefore, you can create complex structures with internal invariants performed by their creator functions without worrying about any other part of the code that violates these invariants. Again, Data.Set provides an example of such code; the general functions in Data.Set do not need to worry about checking the validity of Set objects, because the only functions that can create Set are in the same module and, therefore, can be trusted to get the right solution.

One trade-off between partial and unsafe is the use of "Control.Exception.assert", which the GHC considers as a special case, providing useful error messages for statement failures, but disabling checks when optimizations are turned on. See GHC docs for more details.

+1
source

Source: https://habr.com/ru/post/1493650/


All Articles