Haskell中Monoid与Semigroup依赖疑问:自定义ComplexNumber编译问题
我想要实现一个名为ComplexNumber的自定义数据类型,定义如下:
data ComplexNumber a = C (a, a)
我尝试直接为其实现Monoid实例,定义单位元mempty和二元运算mappend:
instance Num a => Monoid (ComplexNumber a) where mempty = C (0,0) mappend = (C (a1, b1)) (C (a2, b2)) = C (a1 + a2, b1 + b2)
但这段代码无法编译。之后我添加了Semigroup实例的实现,代码可以正常编译运行:
instance Num a => Semigroup (ComplexNumber a) where (C (a1, b1)) <> (C (a2,b2)) = C (a1 + a2, b1 + b2) instance Num a => Monoid (ComplexNumber a) where mempty = C (0,0)
但当我移除Semigroup实例的实现时,会出现如下编译错误:
* Could not deduce (Semigroup (ComplexNumber a)) arising from the superclasses of an instance declaration from the context: Num a bound by the instance declaration at Aufgabe_10.hs:9:10-42 * In the instance declaration for `Monoid (ComplexNumber a)' | 9 | instance Num a => Monoid (ComplexNumber a) where |
请问为何必须实现Semigroup实例才能让Monoid实例编译通过?Semigroup到底是什么?
1. 什么是Semigroup
Semigroup是Haskell标准库中的一个类型类,代表满足结合律的二元运算集合。它只要求实现一个运算符<> (读作"append"),这个运算必须满足结合律:对任意的x、y、z,都有x <> (y <> z) = (x <> y) <> z。
举个例子,字符串的拼接、列表的追加,都是典型的Semigroup实例——它们的组合操作符合结合律,不需要额外的单位元。
2. Monoid与Semigroup的依赖关系
从Haskell base库4.11版本开始,Monoid的定义被修改为以Semigroup作为超类。也就是说:任何要实现Monoid的类型,必须先成为Semigroup的实例。
Monoid本质是Semigroup的扩展:在满足结合律的二元运算基础上,多了一个单位元mempty。这个单位元要满足:对任意值x,x <> mempty = x且mempty <> x = x。
现在Monoid中的mappend已经是Semigroup里<> 的别名,默认实现就是mappend = (<>),所以你不需要在Monoid实例中重复定义二元运算,只要类型已经是Semigroup,再提供正确的mempty即可。
3. 你第一次代码编译失败的原因
首先你写的mappend语法本身有错误(正确写法应该是mappend (C (a1,b1)) (C (a2,b2)) = ...,你多写了一个等号);即便修正语法,在新版本Haskell中,因为Monoid要求类型必须先实现Semigroup,编译器找不到对应的Semigroup实例,依然会报错。
当你添加Semigroup实例后,类型满足了Monoid的超类约束,Monoid会自动复用Semigroup的<> 作为mappend,所以代码就能正常编译了。
内容的提问来源于stack exchange,提问作者AJ.beProgramming

