4

这是一个简单的问题,我认为答案很复杂。

一个非常常见的编程问题是返回某些内容或未通过前置条件检查的函数。IllegalArgumentException在 Java 中,我会使用一些在方法开头抛出的断言函数,如下所示:

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

我喜欢这个的是,它是每个先决条件的单线。我不喜欢的是抛出异常(因为异常〜goto)。

在 Scala 中,我使用过 Either,这有点笨拙,但比抛出异常要好。

有人给我建议:

putStone stone originalBoard = case attemptedSuicide of 
  True  -> Nothing
  False -> Just boardAfterMove
  where {
    attemptedSuicide = undefined
    boardAfterMove = undefined
  }

我不喜欢的是强调真假,它们本身没有任何意义;前提条件隐藏在attemptedSuicide语法之间,因此与 Nothing 没有明确的关系,并且putStone(boardAfterMove) 的实际实现并不是核心逻辑。启动它不会编译,但我确信这不会破坏我的问题的有效性。

在 Haskell 中可以干净地完成前置条件检查的方法是什么?

4

4 回答 4

7

在 Haskell 中,使用MaybeandEither比 Scala 更灵活一些,所以也许你可能会重新考虑这种方法。如果您不介意,我将使用您的第一个示例来说明这一点。

首先,您通常不会测试 null。相反,您只需计算您真正感兴趣的属性,Maybe用于处理故障。例如,如果你真正想要的是列表的头部,你可以写这个函数:

-- Or you can just import this function from the `safe` package

headMay :: [a] -> Maybe a
headMay as = case as of
    []  -> Nothing
    a:_ -> Just a

对于纯粹是验证的东西,比如hasText,那么你可以使用guard,它适用于任何MonadPlus类似的东西Maybe

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

当您专注guardMaybemonad时,就会return变成:JustmzeroNothing

guard precondition = if precondition then Just () else Nothing

现在,假设我们有以下类型:

foo :: [A]
bar :: SomeForm

hasText :: SomeForm -> Bool

magic :: A -> SomeForm -> B

我们可以处理两者的错误,foo并使用monad的符号bar安全地提取magic函数的值:doMaybe

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

如果您熟悉 Scala,do则符号类似于 Scala 的推导式。上面的代码去糖:

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

Maybemonad 中,(>>=)return有以下定义:

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

return = Just

...所以上面的代码只是简写:

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)

...您可以将其简化为:

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

do...这可能是您在没有or的情况下手写的内容guard

于 2013-07-26T13:26:48.943 回答
6

你有两个选择:

  1. 在您的类型中编码您的先决条件,以便在编译时检查它们。
  2. 在运行时检查您的先决条件是否成立,以便您的程序在做一些令人讨厌和意外的事情之前停止。加布里埃尔冈萨雷斯详细展示了他的答案

选项 1. 当然是首选,但并非总是可行。例如,你不能在 Haskell 的类型系统中说一个参数大于另一个参数,等等。但你仍然可以表达很多,通常比其他语言要多得多。还有一些语言使用所谓的依赖类型,并允许您在其类型系统中表达任何条件。但它们大多是实验或研究工作。如果你有兴趣,我建议你阅读Adam Chlipala 的《 Certified Programming with Dependent Types 》一书。

进行运行时检查更容易,而且程序员更习惯这样做。在 Scala 中,您可以require在方法中使用并从相应的异常中恢复。在 Haskell 中,这更棘手。异常(由失败的模式保护引起,或由调用erroror发出undefined)本质上是IO基于它们的,因此只有IO代码可以捕获它们。

如果您怀疑您的代码可能由于某些原因而失败,最好使用MaybeEither向调用者发出失败信号。缺点是这会使代码更复杂,可读性更差。

一种解决方案是将您的计算嵌入到错误处理/报告单子中,例如MonadError. 然后您可以干净地报告错误并在更高级别的某个地方捕获它们。如果您已经在使用 monad 进行计算,则可以将 monad 包装到EitherT转换器中。

于 2013-07-26T13:28:02.673 回答
5

您可以在开始时处理模式保护中的所有先决条件:

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

putStone stone originalBoard = Just ...
于 2013-07-26T12:46:21.213 回答
2

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.

于 2013-07-27T08:24:11.940 回答