Haskell的types检查器允许非常错误的typesreplace,并且程序仍然编译
在我的程序中尝试debugging一个问题时(使用Gloss的两个相同半径的圆正被绘制成不同的大小),我偶然发现了一个奇怪的情况。 在处理对象的文件中,我对Player
有如下的定义:
type Coord = (Float,Float) data Obj = Player { oPos :: Coord, oDims :: Coord }
并在我的主文件,其中导入Objects.hs,我有以下定义:
startPlayer :: Obj startPlayer = Player (0,0) 10
发生这种情况是因为我添加和更改了播放器的字段,忘记更新startPlayer
之后(它的尺寸是由一个单一的数字来表示一个半径,但是我把它改成了一个代表宽度,高度的Coord
;如果我永远让玩家对象成为非圆形)。
令人惊奇的是,上面的代码编译并运行,尽pipe第二个字段是错误的types。
我首先想到,也许我打开了不同版本的文件,但任何文件的任何更改都反映在编译的程序中。
接下来我想可能是因为某些原因, startPlayer
没有被使用。 尽pipe如此,注释掉startPlayer
产生编译器错误,甚至更奇怪的是,在startPlayer
更改10
会导致相应的响应(更改Player
的开始大小)。 再次,尽pipe它是错误的types。 为了确保正确读取数据定义,我在文件中插入了一个错字,它给了我一个错误; 所以我正在看正确的文件。
我尝试将上面的2个代码片段粘贴到自己的文件中,并且抛出了startPlayer
中Player
的第二个字段不正确的错误。
什么可能允许这发生? 你会认为这是Haskell的types检查器应该防止的事情。
这可能编译的唯一方法是如果存在一个Num (Float,Float)
实例。 这不是由标准库提供的,尽pipe可能出于某种疯狂原因使用了其中一个库。 尝试在ghci中加载你的项目,看看10 :: (Float,Float)
工作,然后尝试:i Num
要找出实例来自哪里,然后大叫谁定义它。
附录:无法closures实例。 甚至没有办法不从模块中导出它们。 如果这是可能的话,会导致更令人混淆的代码。 这里唯一真正的解决scheme是不定义这样的实例。
Haskell的types检查器是合理的。 问题是,你正在使用的图书馆的作者做了一些…不那么合理。
简短的回答是:如果有一个实例Num (Float, Float)
,则10 :: (Float, Float)
是完全有效的。 从编译器或语言的angular度来看,没有什么“非常错误的”。 这与数字文字所做的直觉不符。 既然你已经习惯了types系统捕捉你犯的那种错误,你有理由感到惊讶和失望!
Num
实例和fromInteger
问题
你很惊讶,编译器接受10 :: Coord
,即10 :: (Float, Float)
。 假设像10
这样的数字文字被推断为具有“数字”types是合理的。 开箱即可,数字文字可以被解释为Int
, Integer
, Float
或Double
。 没有其他上下文的数字元组在数字方面看起来不像数字。 我们不是在谈论Complex
。
幸运的是,不幸的是,Haskell是一种非常灵活的语言。 该标准指定像10
这样的整数文字将被解释为fromInteger 10
,其types为Num a => a
。 所以10
可以推断为任何types的Num
实例为它写的。 我在另一个答案中更详细地解释了这一点。
所以当你发布你的问题时,一个经验丰富的Haskeller马上发现,要接受10 :: (Float, Float)
,必须有一个像Num a => Num (a, a)
或Num (Float, Float)
的实例。 Prelude
没有这样的实例,所以它一定是在其他地方定义的。 使用:i Num
,你很快发现它来自哪里: gloss
包。
键入同义词和孤立实例
但是等一下。 在这个例子中你没有使用任何gloss
types; 为什么gloss
实例会影响你? 答案分两步走。
首先, 使用关键字type
引入的types同义词不会创build新的types 。 在你的模块中,编写Coord
只是简单的(Float, Float)
。 同样在Graphics.Gloss.Data.Point
, Point
表示(Float, Float)
。 换句话说,你的Coord
和gloss
Point
是字面上相同的。
所以当gloss
维护者select写instance Num Point where ...
,他们也使得你的Coord
types成为Num
一个实例。 这相当于instance Num (Float, Float) where ...
或instance Num Coord where ...
(默认情况下,Haskell不允许types同义词是类实例, gloss
作者必须启用一对语言扩展, TypeSynonymInstances
和FlexibleInstances
来写实例。)
其次,这是令人惊讶的,因为它是一个孤儿实例 ,即一个实例声明instance CA
,其中C
和A
都在其他模块中定义。 这里特别隐晦,因为涉及的每个部分,即Num
, (,)
和Float
,都来自Prelude
并且很可能在任何地方。
你的期望是Num
是在Prelude
定义的,元组和Float
是在Prelude
中定义的,所以在Prelude
中定义了这三件事情的一切。 为什么导入一个完全不同的模块会改变什么? 理想的情况是不会的,但孤儿实例打破了这种直觉。
(请注意,GHC会对孤立实例发出警告 – gloss
的作者会明确否定该警告,这应该会引起红旗,并在文档中至less提示警告。)
类实例是全局的,不能被隐藏
此外,类实例是全局的 :在任何模块中定义的任何实例都可以从模块中传递导入,并且在进行实例parsing时可以在types检查器中使用。 这使得全局推理变得方便,因为我们可以(通常)假定像(+)
这样的类函数对于给定的types总是相同的。 但是,这也意味着地方决策具有全球效应; 定义一个类实例将不可避免地改变下游代码的上下文,而无法将其隐藏或隐藏在模块边界之后。
您不能使用导入列表来避免导入实例 。 同样,你也不能避免从你定义的模块中导出实例。
这是一个Haskell语言devise中有问题和讨论的领域。 关于这个reddit线程中的相关问题有一个迷人的讨论。 例如,参见Edward Kmett关于允许对实例进行可视性控制的评论:“你基本上抛弃了我写的几乎所有代码的正确性。”
(顺便说一下,正如这个答案所表明的那样 ,通过使用孤立实例你可以在某些方面打破全局实例的假设!)
为图书馆实施者做些什么
在实施Num
之前请三思。 你不能解决fromInteger
问题 – 不,定义fromInteger = error "not implemented"
不会使它更好。 你的用户会感到困惑或惊讶,或者更糟糕的是,从来没有注意到,如果他们的整数文字意外推断有你正在实例化的types? 是提供(*)
和(+)
关键 – 特别是如果你必须破解它?
考虑使用像Conal Elliott的vector-space
(typestypes*
)或Edward Kmett的linear
(typestypes* -> *
)类库中定义的替代算术运算符。 这是我倾向于自己做的。
使用 – -Wall
。 不要实现孤立实例,也不要禁用孤立实例警告。
或者,遵循linear
和许多其他良好行为的库,并在以.OrphanInstances
或.Instances
结尾的单独模块中提供孤立实例。 不要从任何其他模块导入该模块 。 然后,用户可以根据需要明确导入孤儿。
如果你发现自己定义孤儿,考虑要求上游维护者实施它们,如果可能和适当的话。 我曾经经常写孤立实例Show a => Show (Identity a)
,直到他们把它添加到transformers
。 我甚至可能提出了一个关于它的错误报告。 我不记得了
为图书馆消费者做些什么
你没有太多的select。 向图书馆维护人员提出礼貌和build设性的要求! 指出他们这个问题。 他们可能有一些特殊的理由来写这个有问题的孤儿,或者他们可能没有意识到。
更广泛地说:请注意这种可能性。 这是Haskell真正的全球效应的less数几个领域之一; 您必须检查导入的每个模块以及这些模块导入的每个模块,都不会执行孤立实例。 types注释有时可能会提示您遇到问题,当然您可以使用:i
在GHCi中进行检查。
如果足够重要,定义自己的新type
而不是type
同义词。 你可以肯定没有人会惹他们。
如果您经常遇到来自开源库的问题,那么您当然可以制作自己的库版本,但是维护很快就会变得令人头疼。