【问题标题】:How can I trigger a type error in Haskell?如何在 Haskell 中触发类型错误?
【发布时间】:2017-08-08 00:04:55
【问题描述】:

假设我有一个类型

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 创造性地使用类型运算符。我接受第一个,但我都喜欢。

【问题讨论】:

  • 值不会导致 Haskell 中的类型错误,因为值和类型不会交互。你所要求的被称为 dependent typing,这是 Haskell 所没有的(至少,不足以满足你的要求)。
  • 感谢@ReinHenrichs 我确实找到了一些关于依赖类型的论文,但我不能说我理解得足够好,可以决定是否可以在我的案例中使用它。
  • 当然可以,只是没有 Haskell。您可能对Type-Driven Development with Idris一书感兴趣,以实用地介绍依赖类型。
  • @ReinHenrichs 您有时可以从 Haskell 已经提供的依赖类型支持水平中获得惊人的里程数。可能刚好足以让这个示例正常工作(至少取决于实际问题的外观)。正如所写的那样,问题看起来可能涉及"phantom types as 'validation' tagging" 事物的轻微扩展(仅具有多个有效组合,而不仅仅是一个)。不过,它确实有点取决于如何使用这个函数(以及你可以接受的代码复杂程度)。

标签: haskell types compile-time


【解决方案1】:

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

{-# 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 ~ bab 约束为相同类型(~ 是类型级别相等),如果它们不是相同类型,则会出错。它大致类似于看起来像这样的值级别代码(使用您原来的 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,但我不想添加太多额外的东西(这是在 cmets 中讨论了一下)。

综上所述,在许多情况下,@DirkyJerky 提供的Maybe 解决方案是可行的方法(由于增加了类型级操作的复杂性)。

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

【讨论】:

  • 有趣。我需要一些时间来了解你在上面做了什么。我不想做任何特别的事情。我只是想知道是否可以这样做。我知道我可以使用 Maybe。 Rein Heinrich 建议的两种依赖类型和您的上述解决方案都是有希望的。
  • @goteguru 好吧,这在技术上是使用依赖类型(因为有依赖于值的类型)。正如 Rein Henrichs 所说,Haskell 对此没有太多支持,但它有一点。有计划向 Haskell 添加更多依赖类型支持。如果您有任何问题,您可以告诉我。这个答案可能会同时抛出很多东西。
  • 如果您使用DataKindsdata T = AType | BType,您的ValidCombo 类型族可以被重新调整以尽可能减少(几乎)。事实上,它可以写成 open 类型族。它必须以某种奇怪的方式编写才能满足规则,但它可以工作:type family ValidCombo (x :: T) (y :: T) :: Constraint; type instance ValidCombo 'BType t2 = t2 ~ 'AType; type instance ValidCombo t1 'BType = t1 ~ 'AType; type instance ValidCombo 'AType _ = 'AType ~ 'AType; type instance ValidCombo _ 'AType = 'AType ~ 'AType
  • 无论这是写成开放型还是封闭型家族,只要 GHC 识别出 either 参数,它就会减少。仅当您关心在传递 other 而不是 ATypeBType 时减少类型族时,封闭类型才重要,我猜你不...
  • @dfeuer 是的。这就是我所说的“你可以用ConstraintKinds [...] 走得更远一点”(虽然我把扩展名弄错了。我现在把它改成DataKinds),但我没有想一次性抛出太多额外的概念,所以我只是做了一个旁注。不过,您最好填写一些细节。也许我应该在最后添加一个部分。另外我不知道它会作为一个开放式家庭工作,我认为它必须关闭。这很有趣(尤其是你实现它的方式)。
【解决方案2】:

据我所知,执行此操作的“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 数据类型,该怎么办?那时会发生什么?程序编译后不能抛出编译错误。

【讨论】:

  • 当然编译器不能推断出用户输入(因为它是不纯的),这就是例外的用途。但它绝对可以在我的纯代码中完成这一切。
  • @goteguru:不,这仍然是一个无法确定的问题,因为程序可能会陷入无限循环。请参阅emptiness problem
  • 即使类型检查,您的程序也可能陷入无限循环。我不是要证明正确性,我只是想在某些非法情况下产生编译错误。
  • @goteguru:不,人们普遍认为 Haskells 类型系统是图灵完备的。它不是。一些扩展使其图灵完备,但默认的非常简单。
  • @goteguru 您仍然遇到一个基本问题:在某些情况下,您将无法向编译器“证明”某些东西将是 A ...。例如,您可以有一个unknown :: F a,如果某些未经证实(甚至无法确定!)的数学猜想为真,则返回A "Eureka!",如果为假,则返回B。不过,这是否会导致实际问题在某种程度上取决于更广泛的背景。
【解决方案3】:

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

{-# 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

* 这个案例不是必需的,但为了完整起见,我将其包括在内。

现在这是一个类型错误:

  • 返回B,其中应为A,反之亦然
  • 包括f B B(“无法访问的代码”)的情况
  • 拨打f B B,甚至间接拨打f (f B (A 1)) B之类的电话

【讨论】:

  • 哈聪明! :) 但它针对的是我的特定示例,不是吗?现在,如果我将声明更改为f (A _) (A a) = B,我必须引入另一个类型运算符^| 并实现某种类型级别的异或(以定义f 的结果类型)。我对吗?顺便说一句,尽管使用 TypeOperators,是否可以派生实例? deriving(Show) 现在抛出错误:-(
  • @goteguru:是的,您需要对其进行更改以强制执行不同的操作,但类型级逻辑是可重用的。你不需要使用TypeOperators——你可以将它们命名为OrXorAndImplies等。你看到的错误可能是因为GADTs,而不是TypeOperators——它应该说“可能的修复:改用独立的派生声明”,您可以通过启用{-# LANGUAGE StandaloneDeriving #-} 并编写deriving instance (Show a) => Show (F x a) 来做到这一点。基本上,GHC 只需要您提供更多信息(Show a 约束)就能够为 GADT 派生实例。
  • 仅供参考:我能想到的|| 的最佳版本(从尽可能减少它的角度来看)是:type family a || b where 'True || _ = 'True; _ || 'True = 'True; 'False || y = y; x || 'False = x; x || x = x。我觉得这可能是最好的,但我不知道如何证明这一点。
  • 原来这就是Data.Type.Bool中使用的定义!
  • @dfeuer 是否可以在类型级别使用通配符(_)?如果我没记错的话,它给我带来了错误。
猜你喜欢
  • 2020-09-03
  • 2012-01-06
  • 2011-02-08
  • 2013-09-04
  • 2013-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多