Sun 4 May 2008
Kefer asked a question in the comments of my post about (co)monadic (co)strength about Uustalu and Vene's ComonadZip class from p157 of The Essence of Dataflow Programming. The class in question is:
class Comonad w => ComonadZip w where czip :: f a -> f b -> f (a, b)
In response I added Control.Functor.Zip [Source] to my nascent rebundled version of category-extras, which was posted up to hackage earlier today.
Putting aside the dual operation for the moment, we can dispense with the inverse of zip quite simply, for much the same reason that every functor in Haskell is strong, every functor in haskell is unzippable:
unfzip :: Functor f => f (a, b) -> (f a, f b) unfzip = fmap fst &&& fmap snd
On the other hand the question of what Functors are zippable is a little trickier. Allowing for a circular definition between fzip and fzipWith we can start with the following class for which you have to implement at least one of fzip or fzipWith.
class Functor f => Zip f where fzip :: f a -> f b -> f (a, b) fzip = fzipWith (,) fzipWith :: (a -> b -> c) -> f a -> f b -> f c fzipWith f as bs = fmap (uncurry f) (fzip as bs)
Here we set aside the restriction that we only be able to Zip a comonad, and simply require that if the functor in question is a comonad, then it is a "symmetric semi-monoidal comonad", which is to say that zipping and then extracting yields the same result as extracting from each separately. You may note a lot of similarity in the above to the definition for Control.Functor.Zap the Dual functor from the other day.
Now, we can throw ourselves with reckless abandon at the easy cases:
instance Zip Identity where fzipWith f (Identity a) (Identity b) = Identity (f a b) instance Zip [] where fzip = zip fzipWith = zipWith instance Zip Maybe where fzipWith f (Just a) (Just b) = Just (f a b) fzipWith f _ _ = Nothing
But we note that Either causes us to break down, we can't handle the 'mixed' cases of Left and Right cleanly. We can however use the same 'cheat' that makes the Writer Monad work, however, and rely on an instance of Monoid, and leaving the left hand side of the bifunctor unchanged to enable us to define:
instance Monoid a => Zip (Either a) where fzipWith f (Left a) (Left b) = Left (mappend a b) fzipWith f (Right a) (Left b) = Left b fzipWith f (Left a) (Right b) = Left a fzipWith f (Right a) (Right b) = Right (f a b)
and similarly:
instance Monoid a => Zip ((,)a) where fzipWith f (a, c) (b, d) = (mappend a b, f c d)
Unfortunately the instance for ((,)a) is a little less than satisfying, what we really want to say there is that we have a Bifunctor and that it has two parameters that can be zipped together:
class Bifunctor p => Bizip p where bizip :: p a c -> p b d -> p (a,b) (c,d) bizip = bizipWith (,) (,) bizipWith :: (a -> b -> e) -> (c -> d -> f) -> p a c -> p b d -> p e f bizipWith f g as bs = bimap (uncurry f) (uncurry g) (bizip as bs)
Now, we can define a more satisfying instance for (,):
instance Bizip (,) where bizipWith f g (a,b) (c,d) = (f a c, g b d)
However, by its very nature, an instance for Either eludes us.
Now, we can define a "Bifunctor-Functor-Functor Bifunctor" transformer that takes a bifunctor and a pair of functors to wrap it around, and derives a new bifunctor and lift the zippability of each of the parts to the zippability of the whole:
newtype BiffB p f g a b = BiffB { runBiffB :: p (f a) (g b) } instance (Functor f, Bifunctor p, Functor g) => Bifunctor (BiffB p f g) where bimap f g = BiffB . bimap (fmap f) (fmap g) . runBiffB instance (Zip f, Bizip p, Zip g) => Bizip (BiffB p f g) where bizipWith f g as bs = BiffB $ bizipWith (fzipWith f) (fzipWith g) (runBiffB as) (runBiffB bs)
What is interesting about this is that the cofree comonad and free monad can be defined in terms of BiffB given a definition for the fixed point of a bifunctor:
newtype FixB s a = InB { outB :: s a (FixB s a) } instance Bifunctor s => Functor (FixB s) where fmap f = InB . bimap f (fmap f) . outB type Cofree f a = FixB (BiffB (,) Identity f) a type Free f a = FixB (BiffB Either Identity f) a
Then we can define that the fixed point of a zippable bifunctor is a zippable functor:
instance Bizip p => Zip (FixB p) where fzipWith f as bs = InB $ bizipWith f (fzipWith f) (outB as) (outB bs)
Then it immediately follows by the construction for BiffB that every Cofree Comonad of a zippable base functor is Zippable because they are the fixed point of BiffB (,) Identity f. and since (,) is zippable and Identity is zippable, then given f zippable the base bifunctor is zippable, so Cofree f is zippable.
On the other hand, we do not get the same result for the Free Monad, because it is built over BiffB Either Identity f, and Either is not a zippable bifunctor.
We can define some other functors and bifunctors which are zippable, i.e. we can define a "functor-wrapped bifunctor bifunctor":
newtype FunctorB f p a b = FunctorB { runFunctorB :: f (p a b) } liftFunctorB :: Functor f => (p a b -> p c d) -> FunctorB f p a b -> FunctorB f p c d liftFunctorB f = FunctorB . fmap f . runFunctorB instance (Functor f, Bifunctor p) => Bifunctor (FunctorB f p) where bimap f g = liftFunctorB (bimap f g) instance (Zip f, Bizip p) => Bizip (FunctorB f p) where bizipWith f g as bs = FunctorB $ fzipWith (bizipWith f g) (runFunctorB as) (runFunctorB bs)
But the general pattern was set by Either and Maybe. Whenever your functor has a branch you need a way to uniquely determine the way the constant terms combine.
While I think the above yields a pleasingly generic version of zip. I do not believe that I have exhausted the set of possible instances, but yielding them automatically for cofree comonads of zippable functors, and hence for rose trees, streams, was rather nice.
If you have any other instances of note, I would welcome the insight.

May 5th, 2008 at 3:53 pm
your Zip class is almost the same as Applicative:
fzipWith f a b = fmap f a b
Your fzip function is suggested in the Applicative paper as making more sense from a category theory perspective.
This also raises the question of what exactly fzip should do. How do you say that it should ‘zip’? For instance [] is also an applicative functor with the usual Monad-style cartestian product behaviour.
Specifying that unfzip is the inverse of fzip is a good idea. For [] fzip is only a right inverse of unfzip, since you could zip lists of different lengths. The same holds for Maybe: funzip (fzip (Just x) Nothing) /= (Just x, Nothing).
May 5th, 2008 at 4:48 pm
[…] Twan van Laarhoven pointed out that fzip from the other day is a close cousin of applicative chosen to be an inverse of universal construction: unfzip […]
June 5th, 2008 at 2:34 pm
[…] In an earlier post about the cofree comonad and the expression problem, I used a typeclass defining a form of duality that enables you to let two functors annihilate each other, letting one select the path whenever the other offered up multiple options. To have a shared set of conventions with the material in Zipping and Unzipping Functors, I have since remodeled that class slightly: […]