为什么此代码使用UndecidableInstances进行编译,然后生成运行时无限循环?

亚历克西斯·金(Alexis King):

使用UndecidableInstances较早的代码编写代码时,我遇到了一些非常奇怪的东西。我设法无意间创建了一些我认为不应进行类型检查的代码:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

data Foo = Foo

class ConvertFoo a b where
  convertFoo :: a -> b

instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
  convertFoo = convertFoo . (convertFoo :: a -> Foo)

evil :: Int -> String
evil = convertFoo

具体来说,convertFoo函数类型会在给定任何输入以产生任何输出时进行检查,如该evil函数所示。刚开始,我以为也许是我偶然地实现了unsafeCoerce,但这并不是真的:实际上,尝试调用我的convertFoo函数(例如evil 3通过执行诸如之类的操作)只会陷入无限循环。

有点模糊地理解正在发生的事情。我对问题的理解是这样的:

  • ConvertFoo我定义的实例可以在任何输入和输出上使用,a并且b仅受转换函数必须来自a -> Foo的两个附加约束所限制Foo -> b
  • 以某种方式,该定义可以匹配任何输入和输出类型,因此似乎对的调用convertFoo :: a -> Foo正在选择其convertFoo本身的定义,因为无论如何它是唯一可用的定义
  • 由于convertFoo自身会无限调用,因此该函数会进入一个永不终止的无限循环。
  • 由于convertFoo永不终止,因此该定义的类型是最底层的,因此从技术上讲,没有违反任何类型,因此程序进行类型检查。

现在,即使以上理解是正确的,我仍然对为什么整个程序进行类型检查感到困惑。特别是,鉴于不存在这样的实例,我期望ConvertFoo a FooConvertFoo Foo b约束都将失败。

确实理解(至少模糊地),选择一个实例时约束并不重要-仅根据实例头选择实例,然后检查约束,因此我可以看到这些约束可能因为我的ConvertFoo a b实例而很好地解决了,这大概是可以的。但是,那将需要解决同一组约束,我认为这会将类型检查器置于无限循环中,从而导致GHC要么挂在编译中,要么给我一个堆栈溢出错误(我已经看到了后者)之前)。

但是,很显然,类型检查器不会循环,因为它很容易触底并愉快地编译我的代码。为什么?在此特定示例中如何满足实例上下文?为什么这不会给我带来类型错误或产生类型检查循环?

hao :

我完全同意这是一个很大的问题。它说明了我们对类型类的直觉与现实之间的差异。

类型类惊喜

要查看这里发生了什么,将增加以下类型签名的赌注evil

data X

class Convert a b where
  convert :: a -> b

instance (Convert a X, Convert X b) => Convert a b where
  convert = convert . (convert :: a -> X)

evil :: a -> b
evil = convert

显然,Covert a b由于只有此类的一个实例,因此正在选择实例。类型检查器在想这样的事情:

  • Convert a X 如果...则为真
    • Convert a X 是真实的[通过假设正确]
    • 并且Convert X X是真的
      • Convert X X 如果...则为真
        • Convert X X 是真实的[通过假设正确]
        • Convert X X 是真实的[通过假设正确]
  • Convert X b 如果...则为真
    • Convert X X 是真的[从上面是真的]
    • 并且Convert X b为真[假设为真]

类型检查器使我们感到惊讶。我们Convert X X没有定义任何类似的东西,所以我们并不期望它是真的。但这(Convert X X, Convert X X) => Convert X X是一种重言式:它是自动正确的,并且无论在类中定义了什么方法,它都是正确的。

这可能与我们的类型类思维模型不符。我们希望编译器在这一点上呆呆,并抱怨Convert X X不可能是真的,因为我们没有为它定义任何实例。我们希望编译器站在Convert X X,寻找另一个可以走到Convert X X真正位置的地方,并放弃,因为没有其他地方可以走到真正的地方。但是编译器能够递归!递归,循环并图灵完备。

我们祝福类型检查器具有此功能,并使用来做到这一点UndecidableInstances当文档指出可以将编译器发送到循环中时,很容易假设最坏的循环,并且我们假设不良循环始终是无限循环。但是在这里,我们展示了一个甚至更致命的循环,一个终止的循环-只是以一种令人惊讶的方式。

(这在丹尼尔的评论中更加明显地证明了这一点:

class Loop a where
  loop :: a

instance Loop a => Loop a where
  loop = loop

不确定性的危险

这是UndecidableInstances允许的确切情况如果我们关闭该扩展名然后再将其打开FlexibleContexts(本质上仅是语法上的无害扩展),则会收到有关违反Paterson条件之一的警告

...
Constraint is no smaller than the instance head
  in the constraint: Convert a X
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

...
Constraint is no smaller than the instance head
  in the constraint: Convert X b
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

“不小于实例头,”尽管我们可以在脑海中将其重写为“该实例有可能被用来证明其本身的断言,并引起您极大的痛苦,折腾和打字。” Paterson条件共同防止实例解析中的循环。我们在这里的违规说明了为什么它们是必需的,并且我们大概可以咨询一些论文以了解它们为什么足够。

自下而上

至于为什么程序在运行时会无限循环:有一个无聊的答案,在那里我们evil :: a -> b只能信任Haskell类型检查器,所以只能无限循环或引发异常或普遍触底反弹,a -> b除了bottom之外,没有其他值可以存在

一个更有趣的答案是,由于从Convert X X严格意义上讲是正确的,因此其实例定义就是这个无限循环

convertXX :: X -> X
convertXX = convertXX . convertXX

我们可以类似地扩展Convert A B实例定义。

convertAB :: A -> B
convertAB =
  convertXB . convertAX
  where
    convertAX = convertXX . convertAX
    convertXX = convertXX . convertXX
    convertXB = convertXB . convertXX

这种令人惊讶的行为以及如何限制实例解析度(默认情况下没有扩展名)是为了避免这些行为,这也许可以看作是Haskell的类型类系统尚未得到广泛采用的一个很好的理由。尽管它具有令人印象深刻的受欢迎程度和强大功能,但它仍然存在奇怪的角落(无论是在文档,错误消息,语法还是什至在其底层逻辑中),似乎特别不适合我们人类对类型级抽象的看法。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

为什么此代码编译并在执行时给出运行时错误

为什么此 VBA 代码运行时间过长?

Java-让用户编写自己的代码,然后进行编译,然后在同一运行时使用它

Scala代码运行时无需使用scalac进行编译?

为什么Contextmanager抛出运行时错误“ throw()之后生成器没有停止”?

为什么非图片代码不能完全使用运行时修复程序进行ASLR?

为什么此代码进入无限循环

为什么格式错误的XAML似乎会编译,然后在运行时失败?

为什么此记录结构正在编译但出现运行时错误

为什么此代码仅使用冗余map()进行编译?

为什么使用yield return的此代码进行编译?

在'loop controller',恒定运行时间和'constant timer'中使用无限循环进行Jmeter测试。有什么优点以及如何使用这种方法进行调整

为什么使用多个构造函数的此Java代码在VS代码中运行,但无法使用javac进行编译?

为什么在Codechef中提交此代码时出现运行时错误(SIGABRT)?

为什么此代码打印第n个数字会给出运行时错误?

为什么此代码仅在我的CA上运行时才会引发IndexError?

为什么在联机提交此代码时出现运行时错误?(jdk 1.7)

为什么此代码在 URI Online Judge 中给出“运行时错误”

为什么此函数应用程序会在purescript中生成运行时错误?

为什么sbt运行时会使用Slick的代码生成器抛出重叠的输出目录?

如果C ++编译为机器代码,为什么我们需要安装“运行时”?

为什么在此代码中没有除以零的编译时警告或运行时崩溃?

为什么使用--option运行时进行代码签名后无法启动Java应用程序

为什么此代码在运行时会生成很大的可执行文件(大约81M)?

为什么此C代码在调试时能正常运行,但在正常运行时却不能正常运行?

如何使用运行时生成的类型的已编译lambda函数进行分组

为什么我的 swig 生成的 tcl 代码在运行时缺少方法?

为什么此代码会陷入无限循环?

为什么此汇编代码会无限循环?