编译器或库的更“本机”部分(IO 或可以访问黑魔法和实现的函数)是否对这些定律做出假设?打破它们会导致不可能的事情发生吗?
或者它们只是表达了一种编程模式——也就是说,你唯一会因为破坏它们而惹恼的人是使用你的代码并且没想到你会如此粗心的人?
单子法则只是实例应遵循的附加规则,超出了类型系统中可以表达的规则。就
Monad
表达编程模式而言,法则是该模式的部分。这些法则也适用于其他类型类:Monoid
与 Monad
具有非常相似的规则,并且通常期望 Eq
的实例将遵循等式关系所期望的规则,以及其他示例。
因为这些法则在某种意义上是类型类的“一部分”,所以其他代码应该合理地期望它们将成立并采取相应的行动。因此,行为不当的实例可能违反客户端代码逻辑所做的假设,从而导致错误,其责任正确地归咎于实例,而不是使用它的代码。
简而言之,“打破单子法则”通常应理解为“编写有缺陷的代码”。
我将用一个涉及另一个类型类的示例来说明这一点,该示例是根据 Daniel Fischer 在 haskell-cafe 邮件列表上给出的类型类进行修改的。众所周知,标准库包含一些行为不当的实例,即浮点类型的
Eq
和 Ord
。正如您可能猜到的那样,当涉及 NaN 时,就会出现错误行为。考虑以下数据结构:
> let x = fromList [0, -1, 0/0, -5, -6, -3] :: Set Float
其中
0/0
产生 NaN,这违反了 Ord
对 Data.Set.Set
实例所做的假设。这个Set
包含0
吗?
> member 0 x
True
是的,当然有,就在眼前!现在,我们将一个值插入到
Set
:
> let x' = insert (0/0) x
这个
Set
还包含0
,对吗?毕竟我们没有删除任何东西。
> member 0 x'
False
...哦。哦,亲爱的。
编译器不会对法律做出任何假设,但是,如果您的实例不遵守法律,它的行为就不会像 monad 一样——它会做奇怪的事情,否则会给您的用户带来无法正常工作的感觉(例如删除值,或以错误的顺序评估事物)。
此外,假设单子定律成立,用户可能会进行重构,这显然是不合理的。
对于使用更“主流”语言的人来说,这就像实现一个接口,但这样做是错误的。例如,假设您正在使用一个提供 IShape 接口的框架,并实现它。然而,您的draw()方法的实现根本不绘制,而只是实例化您的类的1000多个实例。
框架会尝试使用你的 IShape 并用它做合理的事情,天知道会发生什么。这将是一场有趣的火车残骸观看。
如果你说你是 Monad,你就是在“声明”你遵守它的合同和法律。其他代码将相信您的声明并采取相应的行动。既然你撒了谎,事情就会以意想不到的方式出错。