列表的类型类< - >元组转换

时间:2014-08-17 08:30:41

标签: haskell typeclass

我有一组函数可以在元组和列表之间进行转换:

pack2 :: [a] -> (a, a)
pack2 (a:b:_) = (a, b)

unpack2 :: (a, a) -> [a]
unpack2 (a, b) = [a, b]

pack3 :: [a] -> (a, a, a)
pack3 (a:b:c:_) = (a, b, c)

unpack3 :: (a, a, a) -> [a]
unpack3 (a, b, c) = [a, b, c]

-- and so on

我想稍微概括一下,以便可以实现以下目标:

packN [1, 2, 3, 4] :: (Int, Int) -- => (1, 2)

unpackN (1, 2, 3) -- => [1, 2, 3]

我试图像这样解决它:

class Pack a b where
  packN :: a -> b
  unpackN :: b -> a

instance Pack [a] (a, a) where
  packN = pack2
  unpackN = unpack2

instance Pack [a] (a, a, a) where
  packN = pack3
  unpackN = unpack3

它编译时没有任何错误(MultiParamTypeClasses, FlexibleInstances已启用),但是,当尝试使用时,它会失败:

> unpackN (1, 2)

<interactive>:84:1:
    Could not deduce (Pack a (t0, t1))
      arising from the ambiguity check for ‘it’
    from the context (Pack a (t, t2), Num t2, Num t)
      bound by the inferred type for ‘it’:
                 (Pack a (t, t2), Num t2, Num t) => a
      at <interactive>:84:1-14
    The type variables ‘t0’, ‘t1’ are ambiguous
    When checking that ‘it’
      has the inferred type ‘forall a t t1.
                             (Pack a (t, t1), Num t1, Num t) =>
                             a’
    Probable cause: the inferred type is ambiguous

我怎样才能让它发挥作用?

2 个答案:

答案 0 :(得分:8)

unpack必须以某种方式推断输入类型的返回类型,所以让我们看看如何做到这一点(pack总是以[a]作为输入,所以我们没有机会从中推断出任何东西)。

首先,我们可以声明一个功能依赖:

class Unpack tup a | tup -> a where
    unpack :: tup -> [a]

在此我们承诺,我们只会编写Unpack的实例,以便a始终明确地从tup推断出unpack (a, b)。因此,当我们编写(a, b)时,返回类型将从instance Unpack (a, a) a where unpack (a, b) = [a, b] 的类型中显而易见,并且一切都会很好。

class Unpack tup where
    type Elem tup
    unpack :: tup -> [Elem tup]

instance Unpack (a, a) where
    type Elem (a, a) = a
    unpack (a, b) = [a, b]

其次,我们只能通过元组类型对类进行参数化,并使用类型族明确地计算它的元素类型:

unpack (1, 2) -- type error 

然而,还有一件事需要解决。目前,如果元素类型是多态的,unpack在没有类型注释的情况下不起作用:

Num a => a

这是因为数字的类型为(1, 2),而(Int, Float)的类型也可能为instance Unpack (a, a),这与我们在实例中指定的不同。只有当我们在元组中具有相同的具体单形类型时,我们的tup才匹配。

通常,仅根据实例头的形式选择实例(此处实例头是instance Unpack tup中的-- I assume we use the functional dependencies version here instance a ~ b => Unpack (a, b) a where unpack (a, b) = [a, b] 类型),只有在此之后GHC才会检查实例约束持有。我们可以利用这个优势:我们可以确保即使对于元组中的类型不同的情况我们也有一个实例,然后使用实例约束来强制类型相同:

unpack

现在instance (a ~ b, b ~ c, c ~ d) => Unpack (a, b, c, d) a where unpack (a, b, c, d) = [a, b, c, d] 始终无需注释。


写出大元组的所有类型相等约束可能有点麻烦:

ConstraintKinds

幸运的是,我们可以使用{-# LANGUAGE ConstraintKinds, DataKinds, TypeOperators, TypeFamilies, MultiParamTypeClasses, FunctionalDependencies, UndecidableInstances #-} import GHC.Exts type family AllSame (xs :: [*]) :: Constraint where AllSame '[] = () AllSame '[a] = () AllSame (a ': b ': xs) = (a ~ b, AllSame (b ': xs)) ... instance AllSame [a, b, c, d] => Unpack (a, b, c, d) a where unpack (a, b, c, d) = [a, b, c, d] 和类型系列来编写等号的简写:

ConstraintKinds

这里要知道的是()通过将元组类型视为约束的结合而工作,AllSame作为始终满足的空约束。 {{1}}只是一个类型级函数,它扩展到一个等同于我们之前手写的约束。

答案 1 :(得分:2)

这是一个使用类型系列的版本(可能不是惯用的方式)来解决您的问题:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}

module Test where

pack2 :: [a] -> (a, a)
pack2 (a:b:_) = (a, b)

unpack2 :: (a, a) -> [a]
unpack2 (a, b) = [a, b]

pack3 :: [a] -> (a, a, a)
pack3 (a:b:c:_) = (a, b, c)

unpack3 :: (a, a, a) -> [a]
unpack3 (a, b, c) = [a, b, c]

class Pack b where
  type Element b :: *
  packN :: [Element b] -> b
  unpackN :: b -> [Element b]

instance Pack (a, a) where
  type Element (a,a) = a
  packN = pack2
  unpackN = unpack2

instance Pack (a, a, a) where
  type Element (a,a,a) = a
  packN = pack3
  unpackN = unpack3

所以这会起作用:

unpackN ((1,2) :: (Int, Int))
> [1,2]

也许这是一个更好的例子(因为它不共享通用数字缺陷 ^^):

*Test> unpackN ('a','b')
"ab"
*Test> unpackN ('a','b','c')
"abc"