r/haskell•
lfborjas

Which functors are "strong with respect to Either"?

I'm reading the Profunctor Optics paper and in it they implement an instance of Strong (they call it Cartesian) for the Star (aka UpStar) of a functor, like so:

instance Functor f => Cartesian (UpStar f) where
  first (UpStar u)  = UpStar (rstrength . cross u id)
  second (UpStar u) = UpStar (lstrength . cross id u)

-- some givens:
newtype UpStar f a b = UpStar {unUpStar :: a   -> f b}
newtype Costar f a b = Costar {unCostar :: f a -> b}

instance Functor f => Profunctor (UpStar f) where
  dimap f g (UpStar h) = UpStar (fmap g . h . f)

rstrength :: Functor f => (f a, b) -> f (a, b)
rstrength (fx, y) = fmap (,y) fx

lstrength :: Functor f => (a, f b) -> f (a, b)
lstrength (x, fy) = fmap (x,) fy

cross :: (t1 -> a) -> (t2 -> b) -> (t1, t2) -> (a, b)
cross f g (a,b) = (f a, g b)

The paper doesn't go into the dual of Star much (profunctors calls it Costar,) however, the docs for profunctors say in passing that Choice, aka Cocartesian, is "The generalization of Costar of Functor that is strong with respect to Either."

Which means one could do something like this:

instance Functor f => Choice (Costar f) where
  left (Costar c) = Costar magicL
  right (Costar c) = Costar magicR

magicR :: f (Either c a) -> Either c b
magicR = error "questionable"

magicL :: f (Either a c) -> Either b c
magicL = error "also questionable"

But surely those magic functions are too coarse/not applicable to all functors, since I can't figure out how to implement them for a general f!

My question is... I can't quite think of examples of functors for which one can write instance Choice (Costar f), or even come up with a composition of parts like they approached it for Strong + Star. There used to be a "highly questionable" instance for Traversable in profunctors -- I don't quite get why it was highly questionable, maybe it didn't uphold the profunctor/choice laws?

Interestingly, both the paper and profunctors define the instance Applicative f => instance Choice (Star f), because it's possible to "put" something into f via pure in Applicative, but not in a more general Functor; so maybe the Costar route works for stuff where one can "take" something out of a functor? (That is probably wrong: there's surely a reason why the Choice instance for Comonad doesn't go this route!)

Anyhoo, I'm sure there's some really evident things I'm not seeing here, curious to see what the intuition is for which functors qualify for "strong with respect Either" to someone out there better at the more categorical theoretic side of Haskell!

Posted Dec 12, 2021, 12:49 AM UTC · edited

207
viercc•

This quote from documentation Data.Profunctor.Choice might be the issue.

The generalization of Costar of Functor that is strong with respect to Either.

I guess "The generalization of Costar of Functor that is costrong with respect to Either." was the intention behind them.

A Functor f is strong w.r.t. Either means it has

strengthE :: Either (f a) c -> f (Either a c)

f is costrong w.r.t. Either means

costrengthE :: f (Either a c) -> Either (f a) c

where each has to obey associativity law. In fact

left'
  :: Costar f a b -> Costar f (Either a c) (Either b c) 
   ~ (f a -> b) -> f (Either a c) -> Either b c
   ~ f (Either a c) -> Either (f a) c

The last isomorphism is by Yoneda (∀b. (f a -> b) -> Either b c) ~ Yoneda (Either _ c) (f a) ~ Either (f a) c.

I'm not sure there's a well known subclass of Functor which has costrengthE, but I can imagine a subclass of Traversable t with

affineTraverse :: (Functor f) => (a -> f b) -> t a -> Either (t b) (f (t b))
-- either pure id . affineTraverse f = traverse f

which implies (if I'm not missing some detail) a lawful costrengthE using the "highly questionable" instance in the linked history.

6
ineffective_topos•

There is an implementation of Choice (Costar f) for any Comonad. Some Functors may also have instances. Notably Maybe implements this with something that feels intuitively reasonable (but it doesn't seem to nicely implement the more general typeclasses below).

See these links for some related typeclasses: https://fplab.bitbucket.io/posts/2007-07-08-decisive-functors.html https://www.reddit.com/r/haskell/comments/czgiyd/what_is_the_relation_between_decisive_and/

In this case, what we can do is first, extract out from the functor and check whether it's Left or Right. If it's Right, we have a value of type c, so we just return Right c. But if it's Left a, then we replace all Rights in the data structure by Left a, with those implementations listed there.

left (Costar f) = Costar (\fac ->
  case extract fac of
    Right c -> Right c
    Left a -> Left . f $ fmap (either id (const a)) fac)

But a really key point to keep in mind here is that there is a not a single natural choice of how to implement Choice for these functors. Unlike with Functor and Applicative, it's not unique. The arbitrary choice was in how we got rid of Rights for the Left a case. There are multiple solutions there but this comment is too small to contain the details.

4
viercc•

I checked Comonad w => Choice (Cokleisli w) instance, which is equivalent to your version. I think it doesn't satisfy the Choice laws.

gh issue

I doubt there is a lawful instance on Comonad w constraint, because Traced comonad seems to have no lawful Choice (Cokleisli (Traced m)) instance.

... was completely wrong!

1
ineffective_topos•

Actually, I'm not sure it's equivalent? The implementation above should obey the Choice law in question.

2
viercc•

Ahh, you're absolutely right, it's not equivalent and that's my source of confusion.

2
ineffective_topos•

Although I don't have the proof on hand, I was able to manually prove the correctness of this implementation against the necessary associativity laws for Choice.

Could you point out which law of the Choice profunctor (not ArrowChoice) is failing?I would guess you mean rmap Left ≡ lmap Left . left'? In that case, that's fair, and it's a good point, but I don't know the origin of that law as it doesn't seem to derive from the standard definition of costrength.

1
viercc•

rmap Left ≡ lmap Left . left' is another way of saying dimap Left unLeft . left' = id where unLeft :: Either a Void -> a.

2
[deleted]•

[deleted]

1