关联类型和容器元素

时间:2012-01-26 10:09:22

标签: haskell types containers

我想我可能在某个时候在Haskell-Cafe上问过这个问题,但是如果我现在能找到答案那该死的......所以我在这里再问一遍,所以希望将来我能找到< / em>答案!

Haskell在处理参数多态时非常棒。但麻烦的是并非一切都是参数化的。作为一个简单的例子,假设我们想要从容器中获取数据的第一个元素。对于参数类型,这是微不足道的:

class HasFirst c where
  first :: c x -> Maybe x

instance HasFirst [] where
  first []    = Nothing
  first (x:_) = Just x

现在尝试为ByteString编写一个实例。你不能。它的类型没有提到元素类型。您也无法为Set编写实例,因为它需要Ord约束 - 但是类头没有提到元素类型,因此您无法限制它。

关联类型提供了一种完整解决这些问题的简洁方法:

class HasFirst c where
  type Element c :: *
  first :: c -> Maybe (Element c)

instance HasFirst [x] where
  type Element [x] = x
  first []    = Nothing
  first (x:_) = Just x

instance HasFirst ByteString where
  type Element ByteString = Word8
  first b = b ! 0

instance Ord x => HasFirst (Set x) where
  type Element (Set x) = x
  first s = findMin s

然而,我们现在遇到了一个新问题。考虑尝试“修复”Functor,以便它适用于所有容器类型:

class Functor f where
  type Element f :: *
  fmap :: (Functor f2) => (Element f -> Element f2) -> f -> f2

这根本不起作用。它说如果我们有一个从元素类型f到元素类型f2的函数,那么我们可以将f转换为f2。到现在为止还挺好。但是,显然无法要求ff2同样的容器!

根据现有的Functor定义,我们有

fmap :: (x -> y) -> [x] -> [y]
fmap :: (x -> y) -> Seq x -> Seq y
fmap :: (x -> y) -> IO x -> IO y

但我们fmap :: (x -> y) -> IO x -> [y]。那是不可能的。但上面的类定义允许它。

有人知道如何向类型系统解释我实际上的含义吗?

修改

以上工作原理是定义一种从容器类型计算元素类型的方法。如果你试图以相反的方式做到这一点会发生什么?定义一个从元素类型计算容器类型的函数?那会更容易吗?

2 个答案:

答案 0 :(得分:22)

嗯,问题在于,不清楚修订后的Functor是什么意思。例如,考虑ByteString。只能通过将每个ByteString元素替换为相同类型的元素来映射Word8 。但Functor适用于参数可映射结构。这里有两个相互矛盾的映射概念:

  • 刚性映射(即转换结构的每个元素而不改变其类型)
  • 参数化映射(即将结构的每个元素转换为任何类型)

所以,在这种情况下,你无法向类型系统解释你的意思,因为它没有多大意义。但是,你可以改变你的意思:))

使用类型系列很容易表达刚性映射:

class RigidMap f where
  type Element f :: *
  rigidMap :: (Element f -> Element f) -> f -> f

就参数化映射而言,有多种方法可以做到这一点。最简单的方法是按原样保留当前Functor。这些类一起涵盖ByteString[]Seq等结构。但是,由于值Set约束,它们都会落在MapOrd上。令人高兴的是,GHC 7.4中的constraint kinds扩展让我们解决了这个问题:

class RFunctor f where
  type Element f a :: Constraint
  type Element f a = ()  -- default empty constraint
  fmap :: (Element f a, Element f b) => (a -> b) -> f a -> f b

在这里,我们说每个实例都应该有一个关联的类型类约束。例如,Set实例将具有Element Set a = Ord a,以表示只有Set实例可用于该类型时才能构造Ord。可以在=>的左侧显示任何内容。我们可以完全按原样定义我们之前的实例,但我们也可以执行SetMap

instance RFunctor Set where
  type Element Set a = Ord a
  fmap = Set.map

instance RFunctor Map where
  type Element Map a = Ord a
  fmap = Map.map

但是,必须使用两个独立的接口进行严格的映射和受限制的参数映射,这非常烦人。事实上,后者是不是前者的概括?考虑Set之间的区别,Ord只能包含ByteStringWord8的实例,而HasFirst只能包含class Mappable f where type Element f :: * type Result f a r :: Constraint map :: (Result f a r) => (Element f -> a) -> f -> r s。当然,我们可以将其表达为另一种约束?

我们对Result f a r应用相同的技巧(即为整个结构提供实例并使用类型族来公开元素类型),并引入一个新的关联约束族:

Ord a

这里的想法是a表达它对值类型所需的约束(如Result [a] b r),而约束生成的容器类型,但它需要;大概是为了确保它具有r s的同类容器的类型。例如,[b]可能要求Result ByteString b rbWord8将要求rByteString(a ~ b) => ... }是a

类型族已经在这里给出了我们需要表达的“是”:类型相等约束。我们可以说b要求instance Mappable [a] where type Element [a] = a type Result [a] b r = r ~ [b] -- The type in this case works out as: -- map :: (r ~ [b]) => (a -> b) -> [a] -> r -- which simplifies to: -- map :: (a -> b) -> [a] -> [b] map = Prelude.map instance Mappable ByteString where type Element ByteString = Word8 type Result ByteString a r = (a ~ Word8, r ~ ByteString) -- The type is: -- map :: (b ~ Word8, r ~ ByteString) => (Word8 -> b) -> ByteString -> r -- which simplifies to: -- map :: (Word8 -> Word8) -> ByteString -> ByteString map = ByteString.map instance (Ord a) => Mappable (Set a) where type Element (Set a) = a type Result (Set a) b r = (Ord b, r ~ Set b) -- The type is: -- map :: (Ord a, Ord b, r ~ Set b) => (a -> b) -> Set a -> r -- (note the (Ord a) constraint from the instance head) -- which simplifies to: -- map :: (Ord a, Ord b) => (a -> b) -> Set a -> Set b map = Set.map 和{{1}}属于同一类型。当然,我们可以在约束族定义中使用它。所以,我们拥有所需的一切;对实例:

{{1}}

完美!我们可以为我们想要的任何类型的容器定义实例,刚性,参数或参数但受限制,并且类型完美地运行。

免责声明:我还没有尝试过GHC 7.4,所以我不知道这些是否实际编译或工作,但我认为基本的想法是合理的。

答案 1 :(得分:6)

你需要constraint kinds,可在ghc 7.4中找到。