如何在Haskell中触发类型错误?

时间:2017-08-08 00:04:55

标签: haskell types compile-time

假设我有一个类型

data F a = A a | B

我实现了这样的函数f :: F a -> F a -> F a

f (A x) B = A x
f B (A _) = B
f (A _) (A x) = A x

然而,f B B这样的事情在逻辑上是不可能的,所以我想:

f B B = GENERATE_HASKELL_COMPILE_ERROR

当然没有用。省略定义或使用f B B = undefined不是解决方案,因为它会生成运行时异常。我想得到编译时类型错误。

编译器拥有所有信息,它应该能够推断出我犯了一个逻辑错误。如果说我声明let z = f (f B (A 1)) B应该是一个立即编译时错误,而不是一些运行时异常可以隐藏在我的代码中多年。

我找到了一些关于合同的信息,但我不知道如何在这里使用它们,我很好奇是否有任何标准的方法在Haskell中这样做。

编辑:事实证明,我试图做的是被称为依赖类型,但在Haskell中还没有完全支持。然而,使用索引类型和几个扩展可以生成类型错误。 David Young的解决方案似乎更直接,而Jon Purdy创造性地使用类型操作符。我接受第一个,但我喜欢两个。

3 个答案:

答案 0 :(得分:10)

这可以通过一些类型技巧来实现,但是否值得它取决于你正在做什么(顺便提一下,你应该提供更多的上下文,以便我们可以帮助确定有多少类型的机器似乎值得使用)

{-# LANGUAGE GADTs           #-}
{-# LANGUAGE TypeFamilies    #-}
{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE ConstraintKinds #-}

import Data.Constraint

data AType
data BType

data F x y where
  A :: a -> F AType a
  B ::      F BType a

type family ValidCombo x y :: Constraint where
  ValidCombo BType ty2 = ty2 ~ AType
  ValidCombo ty1   ty2 = ()

f :: ValidCombo ty1 ty2 => F ty1 a -> F ty2 a -> F ty1 a
f (A x) B     = A x
f B     (A _) = B
f (A _) (A x) = A x

在编译时,既不可能定义f B B = ...,也不可能尝试像f B B那样调用它。您的示例let z = f (f B (A 1)) B将不会键入检查(尽管更复杂的示例可能会遇到问题)。

我做的第一件事是我为F类型构造函数添加了一个额外的参数。这是一个类型索引(在任何地方都没有该类型的值,它只是一个类型级别标记)。我创建了两个不同的空类型(ATypeBType),用作F的幻像类型参数。

类型族ValidCombo充当类型级别的函数(请注意,该定义与定义典型Haskell值级别函数的方式非常相似,但使用的是类型而不是值)。 ()是一个空约束,永远不会导致类型错误(因为空约束总是,平凡地满足)。在类型级别,a ~ b约束ab属于同一类型(~是类型级别相等),如果它们不相同则会出错类型。它大致类似于看起来像这样的值级代码(使用原始的F类型),但是在类型级别:

data Tag = ATag | BTag
  deriving Eq

getTag :: F a -> Tag
getTag (A _) = ATag
getTag B     = BTag

validCombo :: F a -> F a -> Bool
validCombo B tag2 = (getTag tag2) == ATag
validCombo _ _    = True

(这可以减少,但我已经离开了“标签检查”和显式相等,以便进行更清晰的比较。)

您可以使用DataKinds稍微进一步要求F的第一个类型参数为ATypeBType,但我不想添加太多额外的东西(这在评论中有点讨论)。

所有这一切,在很多情况下,@ DirkyJerky提供的Maybe解决方案是要走的路(由于类型级操作的复杂性增加)。

有时这种类型级技术目前在Haskell中甚至不可能完全实现(它可能适用于您提供的示例,但它取决于它将如何使用),但您需要提供更多信息供我们确定。

答案 1 :(得分:4)

据我所知," Haskell方式"这样做不会引发运行时错误,而是返回Maybe F

f的签名更改为f :: F a -> F a -> Maybe (F a), 并添加另一个类型catch:

f (A x) B = Just $ A x
f B (A _) = Just $ B
f (A _) (A x) = Just $ A x
f B B = Nothing
  

编译器具有所有信息

没有编译器没有所有信息。

怎么样,如果你的程序用户决定输入f函数的内容,他们选择了两个B数据类型怎么办?那会发生什么?在编译程序后,您不能抛出编译错误。

答案 2 :(得分:2)

这是另一种类型系列的解决方案,您可能会发现它更简单,因为它只依赖于布尔逻辑。

$ locate libcrypto.so

/home/user/anaconda3/lib/libcrypto.so
/home/user/anaconda3/lib/libcrypto.so.1.0.0
/home/user/anaconda3/pkgs/openssl-1.0.2k-1/lib/libcrypto.so
/home/user/anaconda3/pkgs/openssl-1.0.2k-1/lib/libcrypto.so.1.0.0
/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
/snap/core/2312/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
/snap/core/2381/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
/snap/core/2462/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
/snap/electronic-wechat/7/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
/usr/lib/x86_64-linux-gnu/libcrypto.so

*此案例不是必需的,但我将其包含在内是为了完整性。

现在这是一个类型错误:

  • 返回预期{-# LANGUAGE DataKinds, GADTs, TypeFamilies, TypeOperators #-} data F x a where A :: a -> F 'True a B :: F 'False a f :: ((x || y) ~ 'True) => F x a -> F y a -> F (x || Not y) a f (A a) B = A a f B (A _) = B f (A _) (A a) = A a ---- type family Not a where Not 'True = 'False Not 'False = 'True type family a || b where 'True || 'True = 'True -- * 'True || b = 'True a || 'True = 'True a || b = 'False 的{​​{1}},反之亦然
  • 包含B(“无法访问的代码”)的案例
  • 调用A,甚至间接使用f B B
  • 之类的内容