'Mergeable with sum and product in type indices

Is there anything in Haskell resembling the following type class?

class Mergeable (f :: Type -> Type -> Type) where
  merge :: f a b -> f c d -> f (a, c) (Either b d)

In particular, imagine there is a Site type indexed on some value and a route:

data Site a r 

And we want to "merge" two sites, such that both their data (value) is kept in memory while supporting either of their routes.

instance Mergeable Site where 
  merge site1 site2 = ...

There is another type, called RouteEncoder a r with the same type shape. So I figured I ought to look for a common pattern here ...


EDIT: Full type definitions of Site and RouteEncoder as requested:

data Site a r = Site
  { siteName :: Text,
    siteRender ::
      Some CLI.Action ->
      RouteEncoder a r ->
      a ->
      r ->
      Asset LByteString,
    -- | Thread that will patch the model over time.
    siteModelData :: ModelRunner a,
    siteRouteEncoder :: RouteEncoder a r
  }

type RouteEncoder a r = PartialIsoEnumerableWithCtx a FilePath r

-- | An Iso that is not necessarily surjective; as well as takes an (unchanging)
-- context value.
--
-- Parse `s` into (optional) `a` which can always be converted to a `s`. The `a`
-- can be enumerated finitely. `ctx` is used to all functions.
-- TODO: Is this isomrophic to `Iso (ctx, a) (Maybe a) s (ctx, s)` (plus, `ctx -> [a]`)?
data PartialIsoEnumerableWithCtx ctx s a
  = PartialIsoEnumerableWithCtx (ctx -> a -> s, ctx -> s -> Maybe a, ctx -> [a])

The code with full context can be seen in this PR: https://github.com/srid/ema/pull/81/files (see also PartialIsoFunctor which class probably should be simplified as well).



Solution 1:[1]

assumed understanding: the difference between covariant and contravariant functors, and by extension, bifunctors and profunctors

summary: you want either a bifunctor that's applicative and alternative, or a profunctor from divisible to alternative, depending on whether it's a bifunctor or a profunctor

a functor f, which can take an associative pair (aka product, tuple) of 'f's and return an f of pairs, is called applicative (well, it's the Apply typeclass from 'semigroupoids', applicatives also need a unit)

that is to say, f is equipped with:

pair :: f a -> f b -> f (a,b)
pair = liftA2 (,)

pairUnit :: f ()
pairUnit = pure ()

the equivalent thing for choices (aka sums, either) is similarly related to alternative functors (it's the Alt typeclass from 'semigroupoids'):

pick :: f a -> f b -> f (Either a b)
pick fa fb = fmap Left fa <|> fmap Right fb

pickUnit :: f Void
pickUnit = empty

something of note about these two formulations of applicative and alternative (sans units) is that they're not the whole story: there's also contravariant applicative functors, and contravariant alternative functors, which are the Divisible and Decidable typeclasses respectively from 'contravariant'

an applicative functor knows how to combine parts into a whole, a divisible functor knows how to split a whole into parts

pairCovariant :: ((a,b) -> c) -> f a -> f b -> f c
pairCovariant f fa fb = fmap f (pair fa fb)

-- liftA2 f = pairCovariant (curry f)

-- this is also called 'divide'
pairContravariant :: (c -> (a,b)) -> f a -> f b -> f c
pairContravariant f fa fb = contramap f (pair fa fb)

-- given an inhabitant of a, provide an f a
pure :: a -> f a
pure a = fmap (const a) . pairUnit

-- this is also called 'conquer'
emptyPair :: f a
emptyPair = fmap (const ()) . pairUnit

(note the similarity between pair and zip, and pairCovariant and zipWith - they're the same functions in different clothing, where zip and zipWith describe the ZipList interpretation of applicative lists)

an alternative functor knows how to combine options into a whole, a decidable functor knows how to split a whole into options

pickCovariant :: (Either a b -> c) -> f a -> f b -> f c
pickCovariant f fa fb = fmap f (pick fa fb)

-- aka choose
pickContravariant :: (c -> Either a b) -> f a -> f b -> f c
pickContravariant f fa fb = contramap f (pick fa fb)

empty :: f a
empty = fmap absurd . pickUnit

-- given proof a is uninhabited, provide an f a
-- aka lose
pureChoice :: (a -> Void) -> f a
pureChoice a = contramap a . pureUnit

so you want applicative/divisible on your first argument, and alternative/decidable on your second argument, depending on co/contravariance on each argument

in other words: if it's a bifunctor, applicative and alternative, if it's a profunctor, divisible and alternative

summary of the ideas here: applicative, alternative (sans the applicative superclass), divisible, and decidable functors are all versions of the same thing, made by choosing between pairs and choices, and co/contravariance

aka, applicatives are pairwise monoidal covariant functors, alternatives are choicewise monoidal contravariant functors, etc

further reading:

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Eric Aya