我在Haskell中有一个Type来使Map有一个与键相关联的值。
如果我编译以下代码:
type Mapa k v = Map k [v]
instance Monoid (Mapa k v) where
--mempty :: Mapa k v
mempty = DM.empty
--mappend :: Mapa k v -> Mapa k v -> Mapa k v
mappend a b = DM.unionWith (++) a b
GHCi将抛出:
Illegal instance declaration for `Monoid (Map k [v])'
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use -XFlexibleInstances if you want to disable this.)
In the instance declaration for `Monoid (Map k [v])'
Mapa应该是newtype
还是data
;
或者问题是什么?
答案 0 :(得分:11)
在这种情况下,您执行想要创建newtype
。当您使用type
时,您创建了一个类型同义词,它完全是装饰性的 - Mapa k v
意味着与Map k [v]
完全相同。您想要创建一个新类型,因为法线贴图已经有一个Monoid
实例,它会与您的新实例重叠,导致行为混乱。
由于您的Mapa
类型的行为方式完全不同,您希望它的类型不同:
newtype Mapa k v = Mapa (DM.Map k [v])
接下来,您必须更新实例才能使用新类型。这需要进行两项更改:您必须打开并重新打包类型同义词,并且必须添加Ord k
约束。第二个是必要的,因为映射的键必须具有可比性,因为映射实际上是内部的树 - 它们必须被排序。所以你的新实例看起来像这样:
instance Ord k => Monoid (Mapa k v) where
mempty = Mapa DM.empty
mappend (Mapa a) (Mapa b) = Mapa $ DM.unionWith (++) a b
与Mapa a
匹配可让您访问基础Map
;然后,您可以使用正常的Map
函数。完成后,您只需要再次将其包装在Mapa
中。
使用这样的不同类型,你必须包装和解包有点不方便,但这是你必须支付的价格,以拥有不同的实例。它还使Mapa
表示与普通地图完全不同的东西更加清晰。
一个小样式提示是你可以使用反引号定义函数,如中缀:
Mapa a `mappend` Mapa b = ...
我认为monoid更清楚,因为monoid操作通常用作中缀运算符。
最后,您要使用newtype
而不是data
的原因是因为newtype
没有运行时开销:它只对typechecker有用。从概念上讲,这是有道理的 - 你不是真的在创建一个新类型,而是在不同的实例中以不同的方式使用相同的底层类型。