这是一个简单的问题,我认为答案很复杂。
一个非常常见的编程问题是返回某些内容或无法进行前置条件检查的函数。在Java中,我会使用一些在方法开头抛出IllegalArgumentException
的断言函数,如下所示:
{
//method body
Assert.isNotNull(foo);
Assert.hasText(bar)
return magic(foo, bar);
}
我喜欢这个,它是每个前提条件的一个oneliner。我不喜欢这个是抛出异常(因为异常~goto)。
在Scala中,我使用了Either,它有点笨重,但比抛出异常更好。
有人向我建议:
putStone stone originalBoard = case attemptedSuicide of
True -> Nothing
False -> Just boardAfterMove
where {
attemptedSuicide = undefined
boardAfterMove = undefined
}
我不喜欢的是强调真假,这本身就没有任何意义; attemptedSuicide
前置条件隐藏在语法之间,因此与Nothing没有明显的关系。putStone
(boardAfterMove)的实际实现并不是核心逻辑。要启动它不会编译,但我确信这不会破坏我的问题的有效性。
在Haskell中可以干净地完成前置条件检查的方法是什么?
答案 0 :(得分:7)
在Haskell中,使用Maybe
和Either
比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
当您将guard
专门化为Maybe
monad时,return
变为Just
而mzero
变为Nothing
:
guard precondition = if precondition then Just () else Nothing
现在,假设我们有以下类型:
foo :: [A]
bar :: SomeForm
hasText :: SomeForm -> Bool
magic :: A -> SomeForm -> B
我们可以处理foo
和bar
的错误,并使用magic
monad的do
表示法为Maybe
函数安全地提取值:< / p>
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)
在Maybe
monad中,(>>=)
和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
或guard
的情况下手写的内容。
答案 1 :(得分:5)
您可以在开头处理模式保护中的所有先决条件:
putStone stone originalBoard | attemptedSuicide = Nothing
where attemptedSuicide = ...
putStone stone originalBoard = Just ...
答案 2 :(得分:5)
您有两种选择:
选项1.当然是首选,但并非总是可行。例如,你不能在Haskell的类型系统中说一个参数比其他参数大,等等。但是你仍然可以表达很多,通常比其他语言更多。还有一些语言使用所谓的dependent types,它允许您在其类型系统中表达任何条件。但它们主要是实验或研究工作。如果您有兴趣,我建议您阅读Adam Chlipala撰写的书Certified Programming with Dependent Types。
进行运行时检查更容易,这也是程序员更习惯的。在Scala中,您可以在方法中使用require
并从相应的异常中恢复。在Haskell中这很棘手。例外(由失败的模式保护引起,或通过调用error
或undefined
发出)是基于IO
的性质,因此只有IO
代码可以捕获它们。
如果您怀疑代码因某些原因而失败,最好使用Maybe
或Either
向调用方发出失败信号。缺点是这会使代码更复杂,更不易读。
一种解决方案是将计算嵌入到错误处理/报告monad中,例如MonadError
。然后,您可以干净地报告错误并将其捕获到更高级别的某个位置。如果您已经使用monad进行计算,则可以将monad包装到EitherT
变换器中。
答案 3 :(得分:2)
我将对此采取更广泛的视角。
在Haskell中,我们通常会区分三种类型的函数:
保证总函数为所有参数提供正确的结果。在您的术语中,前提条件以类型编码。这是最好的功能。其他语言使得编写这种函数变得困难,例如因为你无法消除类型系统中的空引用。
保证部分函数能够提供正确的结果或抛出异常。 “头”和“尾”是部分功能。在这种情况下,您将记录Haddock注释中的前提条件。您不必担心测试前提条件,因为如果您违反它,则无论如何都会抛出异常(尽管有时您会进行冗余测试以便为开发人员提供有用的异常消息)。
不安全的功能会产生损坏的结果。例如,Data.Set模块包括函数“fromAscList”,该函数假定其参数已经按升序排序。如果违反此前提条件,则会出现损坏的Set而不是异常。不安全的功能应该在Haddock的评论中清楚标明。显然,您可以通过测试前提条件将不安全的函数转换为部分函数,但在许多情况下,不安全函数的重点是对于某些客户端来说这太昂贵了,因此您可以通过适当的警告为它们提供不安全的函数。
因为Haskell值是不可变的,所以通常不会强制执行不变量。假设在Java中我有一个拥有Bar的类Foo,而Foo有一些额外的数据必须与Bar的内容一致。如果代码的某些其他部分在不更新Foo的情况下修改了Bar,则会以Foo的作者无法阻止的方式违反不变量。 Haskell没有这个问题。因此,您可以创建具有由其创建者函数强制执行的内部不变量的复杂结构,而无需担心违反这些不变量的其他一些代码。同样,Data.Set提供了这种代码的示例; Data.Set中的总函数不需要担心检查Set对象的有效性,因为可以创建Set的唯一函数位于同一个模块中,因此可以信任它以使其正确。
部分和不安全之间的一个妥协是使用“Control.Exception.assert”,GHC将其视为特殊情况,为断言失败提供有用的错误消息,但在启用优化时禁用检查。有关详细信息,请参阅the GHC docs。