类型检查器允许非常错误的类型替换,并且程序仍然可以编译


99

在尝试调试程序中的问题时(使用Gloss将2个半径相等的圆绘制为不同的大小*)时,我偶然发现了一个奇怪的情况。在我处理对象的文件中,我对a具有以下定义Player

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

在导入Objects.hs的主文件中,我具有以下定义:

startPlayer :: Obj
startPlayer = Player (0,0) 10

发生这种情况是由于我添加和更改了玩家的字段,然后忘记了更新startPlayer(它的尺寸由一个数字来表示半径,但是我将其更改为一个Coord表示(宽度,高度);以防万一玩家对象是非圆形)。

令人惊讶的是,尽管第二个字段的类型错误,但是上面的代码仍可以编译并运行。

我首先以为可能打开了不同版本的文件,但是对任何文件的任何更改都反映在已编译的程序中。

接下来,我认为也许startPlayer由于某种原因而没有被使用。注释掉会startPlayer产生编译器错误,甚至很奇怪,更改10in startPlayer会导致适当的响应(更改的起始大小Player);再次,尽管它是错误的类型。为了确保它正确地读取了数据定义,我在文件中插入了一个错字,这给了我一个错误。所以我在看正确的文件。

我尝试将上面的2个代码段粘贴到自己的文件中,它吐出了Playerin 的第二个字段startPlayer不正确的预期错误。

有什么可能允许这种情况发生?您可能认为这是Haskell的类型检查器应避免的事情。


* 我最初的问题的答案是,两个半径相等的圆被绘制成不同的大小,这是因为其中一个半径实际上是负数。


26
正如@Cubic指出的那样,您绝对应该向Gloss维护人员报告此问题。您的问题很好地说明了库的不适当的孤立实例如何使您的代码混乱。
Christian Conkle 2014年

1
做完了 是否可以排除实例?他们可能需要它来使库发挥作用,但是我不需要它。我还注意到他们定义了Num Color。困扰我只是时间问题。
Carcigenicate,2014年

@立方好吧,为时已晚。而且我只是在一周左右的时间里使用更新的Cabal下载了它;所以应该是最新的
Carcigenicate

2
@ChristianConkle光泽的作者有可能不了解TypeSynonymInstances的功能。无论如何,这确实需要取消(使用Pointa newtype或使用其他运算符名称ala linear
Cubic

1
@Cubic:TypeSynonymInstances本身并不是那么糟糕(尽管不是完全无害的),但是当将它与OverlappingInstances结合使用时,事情会变得很有趣。
约翰L,

Answers:


128

可能编译的唯一方法是存在Num (Float,Float)实例。标准库没有提供此功能,尽管您使用的其中一个库可能出于某种疯狂的原因而添加了它。尝试在ghci中加载项目并查看是否可行10 :: (Float,Float),然后尝试:i Num找出实例的来源,然后对定义它的人大喊大叫。

附录:无法关闭实例。甚至没有办法从模块中导出它们。如果可能的话,将导致更加混乱的代码。唯一真正的解决方案是不定义这样的实例。


53
哇。10 :: (Float, Float)yields (10.0,10.0),并:i Num包含该行instance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’Point是Gloss的Coord别名)。认真吗 谢谢。那使我免于一夜未眠。
Carcigenicate 2014年

6
@Carcigenicate虽然允许这样的实例看起来很琐碎,但允许这样做的原因是,以便开发人员可以Num在有意义的地方编写自己的实例,例如,将Angle数据类型约束Double-pi和之间pi,或者有人要编写数据类型代表四元数或其他更复杂的数字类型,此功能非常方便。它也遵循与String/ Text/ 相同的规则ByteString,从易于使用的角度来看,使这些实例有意义,但在这种情况下可能会被滥用。
bheklilr 2014年

4
@bheklilr我知道需要允许Num实例。“ WOW”源于几件事。我不知道您可以创建类型别名的实例,创建Coord的Num实例似乎违反了直觉,而且我没有想到。哦,好,经验教训。
Carcigenicate,2014年

3
通过使用newtype声明Coord代替,可以解决库中孤立实例的问题type
本杰明·霍奇森

3
@Carcigenicate我相信您需要-XTypeSynonymInstances来允许类型同义词的实例,但这对于创建有问题的实例不是必需的。一个实例Num (Float, Float)甚至(Floating a) => Num (a,a)不需要扩展名,但会导致相同的行为。
crockeea 2014年

64

Haskell的类型检查器是合理的。问题是您正在使用的库的作者做了一些……不太合理的事情。

简短的答案是:是的,10 :: (Float, Float)如果存在实例,则完全有效Num (Float, Float)。从编译器或语言的角度来看,这没有什么“非常错误”。它只是与我们对数字文字的作用的直觉不符。由于您习惯于类型系统捕获到您所犯的那种错误,因此,您确实感到惊讶和失望!

Num实例和fromInteger问题

编译器接受10 :: Coord,您会感到惊讶10 :: (Float, Float)。合理地假设,像这样的数字文字10将被推断为具有“数字”类型。开箱即用,数字文本可以被解释为IntIntegerFloat,或Double。在没有其他上下文的情况下,数字元组在这四种类型是数字的方式上看起来并不像数字。我们不是在谈论Complex

幸运的是,幸运的是,Haskell是一种非常灵活的语言。该标准指定将整数类型like 10解释为fromInteger 10,其类型为Num a => a。因此10可以推断为已为其编写实例的任何类型Num。我将在另一个答案中对此进行更详细的说明。

因此,当您发布问题时,经验丰富的Haskeller会立即发现10 :: (Float, Float)要接受该问题,必须有一个诸如Num a => Num (a, a)或的实例Num (Float, Float)。中没有此类实例Prelude,因此它必须已在其他地方定义。使用:i Num,您很快发现了它的来源:gloss包。

输入同义词和孤立实例

但是等一下。gloss在此示例中,您没有使用任何类型;为什么实例gloss影响您?答案分两个步骤。

首先,关键字引入的类型同义词type不会创建新的type。在您的模块中,编写Coord只是的简写形式(Float, Float)。同样在中Graphics.Gloss.Data.PointPoint是指(Float, Float)。换句话说,您CoordglossPoint字面上是等价的。

因此,当gloss维护人员选择编写时instance Num Point where ...,他们还使您的Coord类型成为的实例Num。等于instance Num (Float, Float) where ...instance Num Coord where ...

(默认情况下,Haskell不允许类型同义词作为类实例。gloss作者必须启用一对语言扩展,TypeSynonymInstancesFlexibleInstances,才能编写实例。)

其次,这令人惊讶,因为它是一个孤立实例,即实例声明instance C A,其中CA都在其他模块中定义。这是非常阴险的,因为每个部分参与其中,即Num(,)Float,来自于Prelude并有可能在范围上随处可见。

您的期望是在Num中定义的Prelude,元组和Float在中定义的Prelude,因此有关这三项工作原理的所有信息都在中定义Prelude。为什么导入一个完全不同的模块会发生什么变化?理想情况下不会,但是孤立的实例破坏了这种直觉。

(请注意,GHC对孤儿实例发出警告- gloss专门针对该警告的作者覆盖了该警告。该警告应该发出红色警告,并至少在文档中提示了警告。)

类实例是全局的,不能隐藏

此外,类实例是全球:是及物动词从进口任何模块中定义的任何情况下你的模块做实例分辨率时,会在背景和提供给typechecker。这使全局推理变得很方便,因为我们可以(通常)假设一个类函数(+)对于给定类型始终是相同的。但是,这也意味着本地决策具有全球影响力。定义类实例将不可撤销地更改下游代码的上下文,而无法在模块边界后掩盖或隐藏它。

不能使用导入列表来避免导入实例。同样,您无法避免从定义的模块中导出实例。

这是Haskell语言设计中一个有问题且讨论最多的领域。在这个reddit线程中有一个有趣的相关问题讨论。例如,请参阅Edward Kmett关于允许对实例进行可见性控制的评论:“您基本上将我编写的几乎所有代码的正确性都排除在外了”。

(顺便说一句,如本答案所示,您可以通过使用孤立实例在某些方面打破全局实例的假设!)

该做什么—对于图书馆实施者

在实施之前要三思Num。你不能解决的fromInteger问题-不,定义fromInteger = error "not implemented"没有变得更好。如果用户的整数文字被意外地推断为您要实例化的类型,您的用户会感到困惑或惊讶吗?或更糟糕的是,永远不会注意到?提供内容(*)和提供(+)内容至关重要吗?尤其是如果您必须破解它的时候?

考虑使用在像Conal Elliott vector-space(针对实物类型*)或Edward Kmett linear(针对实物类型* -> *)之类的库中定义的替代算术运算符。这就是我自己要做的事情。

使用-Wall。不要实现孤立实例,也不要禁用孤立实例警告。

或者,遵循linear和许多其他行为规范的库的指南,并在以.OrphanInstances或结尾的单独模块中提供孤立实例.Instances。并且不要从任何其他模块导入该模块。然后,用户可以根据需要显式导入孤儿。

如果您发现自己定义了孤儿,请考虑在可能和适当的情况下,请上游维护者实施这些孤儿。我曾经经常编写孤立实例Show a => Show (Identity a),直到他们将其添加到中transformers。我什至提出了一个错误报告。我不记得了

该做什么—对于图书馆用户

您没有太多选择。有礼貌地和建设性地向图书馆维护者伸出援手。将他们指向这个问题。他们可能有特殊的原因来写有问题的孤儿,或者他们可能只是没有意识到。

更广泛地说:请注意这种可能性。这是Haskell为数不多的真正具有全球影响力的领域之一。您必须检查您导入的每个模块以及这些模块导入的每个模块是否未实现孤立实例。类型注释有时可能会提示您问题,您当然可以:i在GHCi中使用它进行检查。

如果足够重要,请定义自己newtype的,而不是type同义词。您可以确定没有人会惹他们。

如果您经常遇到源自开放源代码库的问题,则当然可以制作自己的库版本,但是维护很快就会成为头疼的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.