views:

270

answers:

4

A couple of times I've found myself wanting a zip in Haskell that adds padding to the shorter list instead of truncating the longer one. This is easy enough to write. (Monoid works for me here, but you could also just pass in the elements that you want to use for padding.)

zipPad :: (Monoid a, Monoid b) => [a] -> [b] -> [(a, b)]
zipPad xs [] = zip xs (repeat mempty)
zipPad [] ys = zip (repeat mempty) ys
zipPad (x:xs) (y:ys) = (x, y) : zipPad xs ys

This approach gets ugly when trying to define zipPad3. I typed up the following and then realized that of course it doesn't work:

zipPad3 :: (Monoid a, Monoid b, Monoid c) => [a] -> [b] -> [c] -> [(a, b, c)]
zipPad3 xs [] [] = zip3 xs (repeat mempty) (repeat mempty)
zipPad3 [] ys [] = zip3 (repeat mempty) ys (repeat mempty)
zipPad3 [] [] zs = zip3 (repeat mempty) (repeat mempty) zs
zipPad3 xs ys [] = zip3 xs ys (repeat mempty)
zipPad3 xs [] zs = zip3 xs (repeat mempty) zs
zipPad3 [] ys zs = zip3 (repeat mempty) ys zs
zipPad3 (x:xs) (y:ys) (z:zs) = (x, y, z) : zipPad3 xs ys zs

At this point I cheated and just used length to pick the longest list and pad the others.

Am I overlooking a more elegant way to do this, or is something like zipPad3 already defined somewhere?

+14  A: 

How about custom head and tail functions (named next and rest in my example below)?

import Data.Monoid

zipPad :: (Monoid a, Monoid b) => [a] -> [b] -> [(a,b)]
zipPad [] [] = []
zipPad xs ys = (next xs, next ys) : zipPad (rest xs) (rest ys)

zipPad3 :: (Monoid a, Monoid b, Monoid c) => [a] -> [b] -> [c] -> [(a,b,c)]
zipPad3 [] [] [] = []
zipPad3 xs ys zs = (next xs, next ys, next zs) : zipPad3 (rest xs) (rest ys) (rest zs)

next :: (Monoid a) => [a] -> a
next [] = mempty
next xs = head xs

rest :: (Monoid a) => [a] -> [a]
rest [] = []
rest xs = tail xs

Test snippet:

instance Monoid Int where
  mempty = 0
  mappend = (+)

main = do
  print $ zipPad [1,2,3,4 :: Int] [1,2 :: Int]
  print $ zipPad3 [1,2,3,4 :: Int] [9 :: Int] [1,2 :: Int]

Its output:

[(1,1),(2,2),(3,0),(4,0)]
[(1,9,1),(2,0,2),(3,0,0),(4,0,0)]
jetxee
+8  A: 

This pattern comes up quite a lot. A solution I learned from Paul Chiusano is as follows:

data OneOrBoth a b = OneL a | OneR b | Both a b

class Align f where
  align :: (OneOrBoth a b -> c) -> f a -> f b -> f c

instance Align [] where
  align f []     []     = []
  align f (x:xs) []     = f (OneL x)   : align f xs []
  align f []     (y:ys) = f (OneR y)   : align f [] ys
  align f (x:xs) (y:ys) = f (Both x y) : align f xs ys

liftAlign2 f a b = align t
  where t (OneL l)   = f l b
        t (OneR r)   = f a r
        t (Both l r) = f l r

zipPad a b = liftAlign2 (,) a b

liftAlign3 f a b c xs ys = align t (zipPad a b xs ys)
  where t (OneL (x,y))   = f x y c
        t (OneR r)       = f a b r
        t (Both (x,y) r) = f x y r

zipPad3 a b c = liftAlign3 (,,) a b c

A little test in ghci:

 *Main> zipPad3 ["foo", "bar", "baz"] [2, 4, 6, 8] [True, False] "" 0 False
 [("foo",2,True),("bar",4,False),("baz",6,False),("",8,False)]
Apocalisp
+2  A: 

There are times when you want to be able to apply a different function to either tail rather than just supply mempty or manual zeroes as well:

zipWithTail :: (a -> a -> a) -> [a] -> [a] -> [a]
zipWithTail f (a:as) (b:bs) = f a b : zipWithTails f as bs
zipWithTail f [] bs = bs
zipWithTail f as _ = as

zipWithTails :: (a -> c) -> (b -> c) -> (a -> b -> c) -> [a] -> [b] -> [c]
zipWithTails l r f (a:as) (b:bs) = f a b : zipWithTails l r f as bs
zipWithTails _ r _ [] bs = fmap r bs
zipWithTails l _ _ as _ = fmap l as

I use the former when I'm doing something like zipWithTail (+) and the former when I need to do something like zipWithTail (*b) (a*) (\da db -> a*db+b*da) since the former can be much more efficient than feeding a default into a function, and the latter a little bit so.

However, if you just wanted to make a more succinct version of what you have, you could probably turn to mapAccumL ,but its not any clearer, and the ++ can be expensive.

zipPad as bs = done $ mapAccumL go as bs
    where go (a:as) b = (as,(a,b))
          go [] b = ([],(mempty,b))
          done (cs, both) = both ++ fmap (\x -> (x, mempty)) cs
Edward Kmett
+1  A: 

A simpler way to do this is with Maybe. I will illustrate with Edward's more general formulation:

import Data.Maybe
import Control.Applicative

zipWithTails l r f as bs = catMaybes . takeWhile isJust $
    zipWith fMaybe (extend as) (extend bs)
  where
    extend xs = map Just xs ++ repeat Nothing
    fMaybe a b = liftA2 f a b <|> fmap l a <|> fmap r b
Yitz