为类型实例化Monoid

时间:2013-05-04 08:00:55

标签: haskell ghci monoids

我在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; 或者问题是什么?

1 个答案:

答案 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有用。从概念上讲,这是有道理的 - 你不是真的在创建一个新类型,而是在不同的实例中以不同的方式使用相同的底层类型。