【问题标题】:How can I implement generalized "zipn" and "unzipn" in Haskell?如何在 Haskell 中实现广义的“zipn”和“unzipn”?
【发布时间】:2016-10-12 06:09:34
【问题描述】:

我在基本的 Haskell 库中找到此文档:

zip :: [a] -> [b] -> [(a, b)]
    zip takes two lists and returns a list of corresponding pairs. If one input list is short, excess elements of the longer list are discarded.

zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
    zip3 takes three lists and returns a list of triples, analogous to zip.

zip4 :: [a] -> [b] -> [c] -> [d] -> [(a, b, c, d)]
    The zip4 function takes four lists and returns a list of quadruples, analogous to zip.

[...snip...]

unzip :: [(a, b)] -> ([a], [b])
    unzip transforms a list of pairs into a list of first components and a list of second components.

unzip3 :: [(a, b, c)] -> ([a], [b], [c])
    The unzip3 function takes a list of triples and returns three lists, analogous to unzip.

unzip4 :: [(a, b, c, d)] -> ([a], [b], [c], [d])
    The unzip4 function takes a list of quadruples and returns four lists, analogous to unzip.

...等等,直到 zip7 和 unzip7。

这是 Haskell 类型系统的基本限制吗?或者有没有办法实现一次 zip 和 unzip,以处理不同的输入配置?

【问题讨论】:

  • 这里的问题是元组。 Haskell 元组被定义为不同的类型,它们没有任何共同点(类型方面),这使得通用 zipN 成为不可能。
  • 之前的相关问题:stackoverflow.com/questions/2468226/…

标签: haskell


【解决方案1】:

2-ary, 3-ary.. n-ary 元组都是不同的数据类型,所以你不能直接统一处理它们,但是你可以引入一个类型类,它提供一个允许定义泛型的接口@987654322 @ 和 unzip。以下是通用 unzip 的外观:

class Tuple t where
  type Map (f :: * -> *) t

  nilMap   :: Proxy t -> (forall a. f a) -> Map f t
  consMap  :: (forall a. a -> f a -> f a) -> t -> Map f t -> Map f t

Mapf 映射元组类型中的所有类型。 nilMap 构造一个包含空值的映射元组(我不知道为什么 Haskell 需要 Proxy t 在那里)。 consMap 接收一个函数、一个元组和一个映射元组,并用函数逐点压缩元组。以下是实例查找 2 元组和 3 元组的方式:

instance Tuple (a, b) where
  type Map f (a, b) = (f a, f b)

  nilMap _ a = (a, a)
  consMap f (x, y) (a, b) = (f x a, f y b)

instance Tuple (a, b, c) where
  type Map f (a, b, c) = (f a, f b, f c)

  nilMap _ a = (a, a, a)
  consMap f (x, y, z) (a, b, c) = (f x a, f y b, f z c)

gunzip 本身:

gunzip :: forall t. Tuple t => [t] -> Map [] t
gunzip  []    = nilMap (Proxy :: Proxy t) []
gunzip (p:ps) = consMap (:) p (gunzip ps)

这看起来很像transpose

transpose :: [[a]] -> [[a]]
transpose  []      = repeat [] -- `gunzip` handles this case better
transpose (xs:xss) = zipWith (:) xs (transpose xss)

基本上就是这样,除了元组。 gunzip 可以用foldr 等价定义如下:

gunzip :: forall t. Tuple t => [t] -> Map [] t
gunzip = foldr (consMap (:)) $ nilMap (Proxy :: Proxy t) []

要定义泛型zip,我们需要一个可拆分数据类型的类型类(Hackage 上有类似的东西吗?)。

class Splittable f g where
  split :: f a -> g a (f a)

例如对于我们有的列表

newtype MaybeBoth a b = MaybeBoth { getMaybeBoth :: Maybe (a, b) }

instance Splittable [] MaybeBoth where
  split  []    = MaybeBoth  Nothing
  split (x:xs) = MaybeBoth (Just (x, xs))

这是我们添加到 Tuple 类型类的内容:

splitMap :: (Biapplicative g, Splittable f g) => Proxy (f t) -> Map f t -> g t (Map f t)

Biapplicative g 约束确保可以将g a bg c d 组合成g (a, c) (b, d)。对于 2- 和 3- 元组,它看起来像这样:

splitMap _ (a, b) = biliftA2 (,) (,) (split a) (split b)

splitMap _ (a, b, c) = biliftA3 (,,) (,,) (split a) (split b) (split c)

在为MaybeBoth 提供Biapplicative 实例之后

instance Biapplicative MaybeBoth where
  bipure x y = MaybeBoth $ Just (x, y)
  MaybeBoth f <<*>> MaybeBoth a = MaybeBoth $ uncurry (***) <$> f <*> a

我们终于可以定义gzip

gzip :: forall t. Tuple t => Map [] t -> [t]
gzip a = maybe [] (\(p, a') -> p : gzip a') . getMaybeBoth $ splitMap (Proxy :: Proxy [t]) a

它重复地切割元组中列表的第一个元素,从它们形成一个元组并将其添加到结果中。

应该可以通过向SplittableUniteable 或类似的东西)添加对偶来概括 gunzip,但我会在这里停下来。

编辑I couldn't stop.

【讨论】:

    【解决方案2】:

    这是应用程序非常有用的一个方面。查看ZipList,它只是一个简单列表的newtype 包装器。包装器的原因是ZipList 有一个应用程序实例,您猜对了,将列表压缩在一起。然后,如果你想要zip7 as bs cs ds es fs gs hs,你可以这样做

    (,,,,,,) <$> as <*> bs <*> cs <*> ds <*> es <*> fs <*> gs <*> hs
    

    如您所知,此机制也适用于扩展zipWith,这是zip 的一般情况。老实说,我认为我们应该去掉所有的 zipN 函数,而是把上面的内容教给人们。 zip 本身很好,但除此之外......

    模板 Haskell 解决方案

    正如 cmets 和其他答案所表明的,这不是一个特别令人满意的答案。我期望别人实现的一件事是TemplateHaskell 版本的zipunzip。还没有人这样做,就在这里。

    它所做的只是机械地为zipunzip 函数生成AST。 zip 背后的想法是使用ZipListunzip 背后的想法是使用foldr

    zip as ... zs === \as ... zs -> getZipList $ (, ... ,) <$> ZipList as <*> ... <*> ZipList zs
    unzip         === foldr (\ (a, ... ,z) ~(as, ... ,zs) -> (a:as, ... ,z:zs) ) ([], ... ,[])
    

    实现如下所示。

    {-# LANGUAGE TemplateHaskell #-}
    module Zip (zip, unzip) where
    
    import Prelude hiding (zip, unzip)
    import Language.Haskell.TH
    import Control.Monad
    import Control.Applicative (ZipList(..))
    
    -- | Given number, produces the `zip` function of corresponding arity
    zip :: Int -> Q Exp
    zip n = do
      lists <- replicateM n (newName "xs")
    
      lamE (varP <$> lists)
           [| getZipList $
                $(foldl (\a b -> [| $a <*> ZipList $(varE b) |])
                        [| pure $(conE (tupleDataName n)) |]
                        lists) |]
    
    -- | Given number, produces the `unzip` function of corresponding arity
    unzip :: Int -> Q Exp
    unzip n = do
      heads <- replicateM n (newName "x")
      tails <- replicateM n (newName "xs")
    
      [| foldr (\ $(tupP (varP <$> heads)) ~ $(tupP (varP <$> tails)) -> 
                    $(tupE (zipWith (\x xs -> [| $x : $xs |])
                                    (varE <$> heads)
                                    (varE <$> tails))))
               $(tupE (replicate n [| [] |])) |]
    

    你可以在 GHCi 试试这个:

    ghci> :set -XTemplateHaskell
    ghci> $(zip 3) [1..10] "abcd" [4,6..]
    [(1,'a',4),(2,'b',6),(3,'c',8),(4,'d',10)]
    ghci> $(unzip 3) [(1,'a',4),(2,'b',6),(3,'c',8),(4,'d',10)]
    ([1,2,3,4],"abcd",[4,6,8,10])
    

    【讨论】:

    • 这并没有概括各种zipN函数;它只是用相应的硬编码元组构造函数替换它们。 (zip(,)zip2(,,) 等。
    • @chepner 我认为一旦内置的zip1zip7 解决方案失败,OP 确实在寻求将列表压缩在一起的方法。考虑到像 ZipList 这样的机制存在于 vanilla Haskell 中就是为了这个目的,我认为推荐一些需要有十几个扩展并且被各种强制性注释所困扰的东西并不是一个好主意。
    • ZipList 是关于如何将函数列表应用于输入列表,而不是关于如何将任意数量的单个函数应用于必要数量的列表。
    • @chepner 同样,避免难以导航的任意数量,我认为这与您所获得的一样接近。甚至文档也将其描述为一种通用的zipWith:f &lt;$&gt; ZipList xs1 &lt;*&gt; ... &lt;*&gt; ZipList xsn = ZipList (zipWithn f xs1 ... xsn)。也就是说,我意识到这不是一个真正的任意数量函数。我什至在答案中这么说。
    【解决方案3】:

    这是一个 zipN 函数,它依赖于 generics-sop 包的机制:

    {-# language TypeFamilies #-}
    {-# language DataKinds #-}
    {-# language TypeApplications #-}
    
    import Control.Applicative
    import Generics.SOP
    
    -- "a" is some single-constructor product type, like some form of n-ary tuple
    -- "xs" is a type-level list of the types of the elements of "a"
    zipN :: (Generic a, Code a ~ '[ xs ]) => NP [] xs -> [a]
    zipN np = to . SOP . Z <$> getZipList (hsequence (hliftA ZipList np))
    
    main :: IO ()
    main = do
       let zipped = zipN @(_,_,_) ([1,2,3,4,5,6] :* ['a','b','c'] :* [True,False] :* Nil)
       print $ zipped
    

    结果:

    [(1,'a',True),(2,'b',False)]
    

    这种解决方案有两个缺点:

    • 您必须将参数列表包装在由:*Nil 构造的generics-sop 中的特殊NP 类型中。
    • 您需要以某种方式指定结果值是元组列表,而不是其他Generic 兼容类型的列表。在这里,它是通过 @(_,_,_) 类型的应用程序完成的。

    【讨论】:

    • 您刚刚将zip3 替换为zip @(_,_,_)。它仍然不通用,您刚刚将函数使用的硬编码元组大小更改为参数的位置。
    • @chepner 从用户的角度来看这并不容易,但它确实减少了实现中重复代码的需要。此外,用户可以在使用该值时指定类型,例如通过模式匹配。
    • 这很公平。我确实认为这可能是最接近 OP 意图的。
    【解决方案4】:

    你说得对,这些函数(zip2、zip3 等)都是相同模式的实例,在理想情况下,它们应该是通用的。顺便说一句,作为给读者的练习,弄清楚 zip1 和 zip0 应该是什么;)。

    但是,很难通用地实现 zipN,因为所有不同情况之间的通用模式相当重要。这并不意味着不可能通用地实现它,但您需要 Haskell GHC 的一些更高级的类型系统功能来实现它。

    更具体地说,zip2、zip3 等都有不同数量的参数,这使其成为“arity-generic programming”的一个实例(函数的arity 是它的参数数量)。正如您在函数式编程世界中所期望的那样,an interesting research paper 恰好涵盖了这个主题(“arity-generic programming”),并且方便地,它们的主要示例之一是...... zipWithN。它没有直接回答您的问题,因为它使用 Agda 而不是 Haskell,但您可能仍然会觉得它很有趣。在任何情况下,类似的想法都可以通过 Haskell 的 GHC 更高级的类型系统功能(想到 TypeFamilies 和 DataKinds)中的一个或多个来实现。 PDF version here.

    顺便说一下,这只是一个 arity-generic zipWithN。对于 arity-generic zipN,您可能需要编译器的一些支持,特别是元组构造函数的 arity-generic 接口,我怀疑它可能不在 GHC 中。这就是我认为 augustss 对问题的评论和 chepner 对 Alec 回答的评论所指的内容。

    【讨论】:

    • 执行zipNzipWithN 困难得多,因为(我认为这是augustss 评论的要点)元组类型是独立的,与递归定义的函数类型不同。
    • 是的,这就是我最后一段的内容。在我看来,编译器可以在那里提供一些帮助,例如通过公开一个原始类型级函数 mkTupleType 类型为 (n : Nat) -&gt; (tys : Vec * n) -&gt; NFun tys *
    • 抱歉,纠正一下自己,我的意思是像mkTupleType :: (n : Nat) -&gt; Vec * n -&gt; * 这样的类型级原语和像mkTuple :: forall (n : Nat) (tys : Vec * n). NFun tys (mkTuple n tys) 这样的值级原语的组合。在这里,NFun 是一个假设的类型级函数,它从参数类型的向量和结果类型(可能以某种形式存在于一些泛型库中)构造函数类型。
    • "zip2、zip3 等都有不同数量的参数,这使得它成为一个通用编程的实例......" 不一定。该函数可以很容易地编写为采用单个参数,即列表列表,就像“解压缩”函数使用它们的返回值一样。
    • 你当然可以这样做,但是你放弃了一些类型安全。据我了解这个问题,它是关于您是否可以在不放弃类型安全的情况下进行 zipn。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-12
    • 2018-01-07
    • 2013-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多