Haskell的类型检查器是合理的。问题是您正在使用的库的作者做了一些……不太合理的事情。
简短的答案是:是的,10 :: (Float, Float)
如果存在实例,则完全有效Num (Float, Float)
。从编译器或语言的角度来看,这没有什么“非常错误”。它只是与我们对数字文字的作用的直觉不符。由于您习惯于类型系统捕获到您所犯的那种错误,因此,您确实感到惊讶和失望!
Num
实例和fromInteger
问题
编译器接受10 :: Coord
,您会感到惊讶10 :: (Float, Float)
。合理地假设,像这样的数字文字10
将被推断为具有“数字”类型。开箱即用,数字文本可以被解释为Int
,Integer
,Float
,或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.Point
,Point
是指(Float, Float)
。换句话说,您Coord
和gloss
的Point
字面上是等价的。
因此,当gloss
维护人员选择编写时instance Num Point where ...
,他们还使您的Coord
类型成为的实例Num
。等于instance Num (Float, Float) where ...
或instance Num Coord where ...
。
(默认情况下,Haskell不允许类型同义词作为类实例。gloss
作者必须启用一对语言扩展,TypeSynonymInstances
和FlexibleInstances
,才能编写实例。)
其次,这令人惊讶,因为它是一个孤立实例,即实例声明instance C A
,其中C
和A
都在其他模块中定义。这是非常阴险的,因为每个部分参与其中,即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
同义词。您可以确定没有人会惹他们。
如果您经常遇到源自开放源代码库的问题,则当然可以制作自己的库版本,但是维护很快就会成为头疼的问题。