5

我在 Haskell 中有一个类型来使 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 应该是 anewtype还是 a data;或者有什么问题?

4

1 回答 1

12

在这种情况下,您确实想要创建一个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 = ...

我认为这对于幺半群来说更清楚,因为幺半群操作通常用作中缀运算符。

最后,你想使用newtype而不是data因为newtype没有运行时开销:它只对类型检查器很重要。从概念上讲,这是有道理的——您实际上并不是在创建一个新类型,而是在不同的实例中以不同的方式使用相同的底层类型。

于 2013-05-04T08:17:10.627 回答