Skip to content

Every witherable is a crosswalk #188

@mniip

Description

@mniip

If we take a list and imagine extending it to fill all missing locations (locations past the end of the list) with a default value, e.g. like xs ++ repeat defx, then such constructions can be losslessly zipped together:

zipWith f (xs ++ repeat defx) (ys ++ repeat defy)
  = alignWith (uncurry f . fromThese defx defy) xs ys ++ repeat (f defx defy)

Of course if we have xs ++ repeat defx we can no longer take it apart into the "main" part and the "filler" part. Instead we may want do bookkeeping to keep them separate like this:

data FillList a = FillList [a] a
  deriving (Functor)
instance Applicative FillList where
  pure x = FillList [] x
  liftA2 f (FillList xs defx) (FillList ys defy)
    = FillList (alignWith (uncurry f . fromThese defx defy) xs ys) (f defx defy)

This easily generalizes to an arbitrary Align:

data Fill f a = Fill (f a) a
  deriving (Functor)
instance Align f => Applicative (Fill f) where
  pure x = Fill nil x
  liftA2 f (Fill xs defx) (Fill ys defy)
    = Fill (alignWith (uncurry f . fromThese defx defy) xs ys) (f defx defy)

Thus we have an Applicative that in a certain sense losslessly captures the Align operation:

alignWith' :: (Functor f, Applicative (Fill f)) => (These a b -> c) -> f a -> f b -> f c
alignWith' f xs ys = case liftA2 (alignWith @Maybe f)
    (Fill (Just <$> xs) Nothing)
    (Fill (Just <$> ys) Nothing)
  of Fill zs _defz -> fromJust <$> zs

The fromJust there is unfortunate but it's valid (it ultimately relies on functoriality and the fact that uncurry (align @Maybe) . fromThese Nothing Nothing . bimap Just Just = Just). (If anyone has any clever ideas regarding how to get rid of it, I'm all ears).

Generalizing to zipping arbitrarily many structures we obtain:

\xss -> case traverse (\xs -> Fill (Just <$> xs) Nothing) xss of
  Fill ys _defy -> ys
:: (Traversable t, Align f) => t (f a) -> f (t (Maybe a))

If t supports a catMaybes operation, i.e. is Filterable, we can turn into f (t a), and this operation in fact coincides with sequenceL:

sequenceL' :: (Traversable t, Filterable t, Align f) => t (f a) -> f (t a)
sequenceL' xs = case traverse (\x -> Fill (Just <$> x) Nothing) xs of
  Fill ys _ -> catMaybes <$> ys

Witherable is essentially Traversable + Filterable, so every Witherable is a Crosswalk. QuickCheck agrees:

> quickCheck $ \xs -> sequenceL' xs === sequenceL @Maybe @Maybe @Int xs
+++ OK, passed 100 tests.
> quickCheck $ \xs -> sequenceL' xs === sequenceL @Maybe @[] @Int xs
+++ OK, passed 100 tests.
> quickCheck $ \xs -> sequenceL' xs === sequenceL @Maybe @(Map Int) @Int xs
+++ OK, passed 100 tests.
> quickCheck $ \xs -> sequenceL' xs === sequenceL @[] @Maybe @Int xs
+++ OK, passed 100 tests.
> quickCheck $ \xs -> sequenceL' xs === sequenceL @[] @[] @Int xs
+++ OK, passed 100 tests.
> quickCheck $ \xs -> sequenceL' xs === sequenceL @[] @(Map Int) @Int xs
+++ OK, passed 100 tests.

An even more remarkable result is that every Crosswalk is almost a Filterable:

almostCatMaybes :: forall t a. Crosswalk t => t (Maybe a) -> Maybe (t a)
almostCatMaybes = sequenceL @t @Maybe @a

where the function returns Nothing in case all of the inputs were Nothing. This is significant for e.g. t ~ NonEmpty. But if we're implementing sequenceL from catMaybes then the catMaybes is never invoked with a container full of Nothing's (for a reason similar to the alignWith' case), so we can use catMaybes = fromJust . almostCatMaybes. This leads us to the final remarkable fact:

The complete behavior of sequenceL can be recovered from its Maybe specialization:

class Crosswalk t where
  {-# MINIMAL almostCatMaybes | sequenceL #-}
  almostCatMaybes :: t (Maybe a) -> Maybe (t a)
  almostCatMaybes = sequenceL

  sequenceL :: Align f => t (f a) -> f (t a)
  default sequenceL :: (Traversable t, Align f) => t (f a) -> f (t a)
  sequenceL xs = case traverse (\x -> Fill (Just <$> x) Nothing) xs of
    Fill ys _ -> fromJust . almostCatMaybes <$> ys

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions