为了提高我对GHC扩展的了解,我决定尝试用单位来实现数字,而我想要做的一件事是使用数字文字来表示无单位值。但由于Haskell所做的开放世界假设,结果证明不太实际。我无法工作的最小例子如下:
data Unit u a = Unit a
data NoUnit
instance Num a => Num (Unit NoUnit a) where
-- (+) (*) (-) abs signum not important
fromInteger = Unit . fromInteger
type family Multiply a b where
Multiply NoUnit NoUnit = NoUnit
multiply :: Num a => Unit u1 a -> Unit u2 a -> Unit (Multiply u1 u2) a
multiply (Unit a) (Unit b) = Unit $ a * b
现在,如果我尝试做multiply 1 1
之类的事情,我希望结果值是明确的。因为获得Num (Unit u a)
类型的唯一方法是将u
设置为NoUnit
。其余的a
应由违约规则处理。
不幸的是,由于Haskell的开放世界假设,有些邪恶的人可能会认为即使是具有单位的数字应该是有效的Num
实例,即使这样的事情会违反(*) :: a -> a -> a
数字与单位的乘法不适合该类型的签名。
现在开放世界的假设不是一个不合理的假设,特别是因为Haskell报告没有禁止孤立实例。但在这种特定情况下,我确实想告诉GHC,Num
Unit
实例唯一有效的幻像单元类型是NoUnit
。
有没有办法明确说明这一点,并且在一些附注中会禁止孤儿实例允许GHC放宽开放世界的假设?
当尝试使用部分依赖键入来使我的程序更安全时,这种事情已经出现过几次。每当我想为基本案例指定Num
或IsString
或IsList
实例,然后使用自定义值或函数来获取所有其他可能的案例。
答案 0 :(得分:14)
你无法关闭开放世界的假设,但有一些方法可以限制它,包括这个时间。在您的情况下,问题在于您编写Num
实例的方式:
instance Num a => Num (Unit NoUnit a)
你真正想要的是
instance (Num a, u ~ NoUnit) => Num (Unit u a)
这样,当GHC发现它需要Num (Unit u) a
时,就会得出结论,它需要Num a
和u ~ NoUnit
。你写它的方式,你在某个地方留下了一些额外实例的可能性。
将左侧的=>
右侧的类型构造函数转换为等式约束的技巧通常很有用。