如何在Haskell中实现通用的“zipn”和“unzipn”?

时间:2016-10-12 06:09:35

标签: haskell

我在基本的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和解压缩一次,以处理不同的输入配置?

4 个答案:

答案 0 :(得分:7)

这是申请人的一个非常有用的方面。查看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解决方案

正如评论和其他答案所示,这不是一个特别令人满意的答案。我期待其他人实施的一件事是TemplateHaskellzipunzip。由于还没有人这样做,所以就是这样。

它所做的就是为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])

答案 1 :(得分: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 - 兼容类型的列表。在这里,它使用@(_,_,_)类型的应用程序完成。

答案 2 :(得分:1)

2-ary,3-ary .. n-ary元组都是不同的数据类型,所以你不能直接统一处理它们,但是你可以引入一个类型类,它提供了一个允许定义泛型{{ 1}}和zip。以下是查找通用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 使用Map映射元组类型中的所有类型。 f构造一个包含空值的Mapped元组(我不知道为什么Haskell要求那里nilMap)。 Proxy t接收一个函数,一个元组和一个映射元组,并用函数逐点压缩元组。以下是实例查找2元组和3元组的方式:

consMap

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) [] ,我们需要一个可拆分数据类型的类型类(在Hackage上有这样的东西吗?)。

zip

E.g。对于我们有的列表

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 gg a b合并到g c d中。对于2和3元组,它看起来像这样:

g (a, c) (b, d)

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

提供Biapplicative个实例后
MaybeBoth

我们最终可以定义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 gunzip或类似的东西)来概括Splittable,但我会在此停止。

编辑I couldn't stop

答案 3 :(得分:0)

你是对的,这些函数(zip2,zip3等)都是相同模式的实例,在理想的世界中,它们应该是可以实现的。顺便说一句,作为读者的练习,找出zip1和zip0应该是什么;)。

然而,一般来说很难实现zipN,因为所有不同情况之间的共同模式是非常重要的。这并不意味着一般不可能实现它,但是你需要一些 Haskell GHC的更高级类型系统功能来实现它。

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

顺便说一下,这只是一个arity-generic zipWithN。对于arity-generic zipN,您可能需要编译器的一些支持,特别是元组构造函数的arity-generic接口,我怀疑它可能不在GHC中。这就是我相信奥古斯都对这个问题的评论以及对亚力克答案所提及的评论。