What's the relationship between profunctors and arrows?
What profunctors lack compared to arrows is the ability to compose them. If we add composition, will we get an arrow?
MONOIDS
This is exactly the question tackled in section 6 of "Notions of Computation as Monoids," which unpacks a result from the (rather dense) "Categorical semantics for arrows". "Notions" is a great paper because while it dives deep into category theory it (1) doesn't assume the reader has more than a cursory knowledge of abstract algebra and (2) illustrates most of the migraine-inducing mathematics with Haskell code. We can briefly summarize section 6 of the paper here:
Say we have
class Profunctor p where
dimap :: (contra' -> contra) -> (co -> co') -> p contra co -> p contra' co'
Your bog-standard, negative-and-positive dividin' encoding of profunctors in Haskell. Now this data type,
data (⊗) f g contra co = forall x. (f contra x) ⊗ (g x co)
as implemented in Data.Profunctor.Composition, acts like composition for profunctor. We can, for example, demonstrate a lawful instance Profunctor
:
instance (Profunctor f, Profunctor g) => Profunctor (f ⊗ g) where
dimap contra co (f ⊗ g) = (dimap contra id f) ⊗ (dimap id co g)
We will hand-wave the proof that it is lawful for reasons of time and space.
OK. Now the fun part. Say we this typeclass:
class Profunctor p => ProfunctorMonoid p where
e :: (a -> b) -> p a b
m :: (p ⊗ p) a b -> p a b
This is, with a lot more hand-waving, a way of encoding the notion of profunctor monoids in Haskell. Specifically this is a monoid in the monoidal category Pro
, which is a monoidal structure for the functor category [C^op x C, Set]
with ⊗
as the tensor and Hom
as its unit. So there's a lot of ultraspecific mathematical diction to unpack here but for that you should just read the paper.
We then see that ProfunctorMonoid
is isomorphic to Arrow
... almost.
instance ProfunctorMonoid p => Category p where
id = dimap id id
(.) pbc pab = m (pab ⊗ pbc)
instance ProfunctorMonoid p => Arrow p where
arr = e
first = undefined
instance Arrow p => Profunctor p where
lmap = (^>>)
rmap = (>>^)
instance Arrow p => ProfunctorMonoid p where
e = arr
m (pax ⊗ pxb) = pax >> pxb
Of course we are ignoring the typeclass laws here but, as the paper shows, they do work out fantastically.
Now I said almost because crucially we were unable to implement first
. What we have really done is demonstrated an isomorphism between ProfunctorMonoid
and pre-arrows .The paper calls Arrow
without first
a pre-arrow. It then goes on to show that
class Profunctor p => StrongProfunctor p where
first :: p x y -> p (x, z) (y, z)
class StrongProfunctor p => StrongProfunctorMonoid p where
e :: (a -> b) -> p a b
m :: (p ⊗ p) a b -> p a b
is necessary and sufficient for the desired isomorphism to Arrow
. The word "strong" comes from a specific notion in category theory and is described by the paper in better writing and richer detail than I could ever muster.
So to summarize:
A monoid in the category of profunctors is a pre-arrow, and vice versa. (A previous version of the paper used the term "weak arrows" instead of pre-arrows, and that's OK too I guess.)
A monoid in the category of strong profunctors is an arrow, and vice versa.
Since monad is a monoid in the category of endofunctors we can think of the SAT analogy
Functor : Profunctor :: Monad : Arrow
. This is the real thrust of the notions-of-computation-as-monoids paper.Monoids and monoidal categories are gentle sea creatures that appear everywhere, and it's a shame that some students will go through computer science or software engineering education without being taught monoids.
Category theory is fun.
Haskell is fun.
@haoformayor's answer (and the referenced paper) is a great insight into the underlying category theory - monoidal categories are rather beautiful! - but I thought some code showing you how to turn an Arrow
into a Strong Category
and vice versa as they appear in their respective libraries might make a helpful addendum.
import Control.Arrow
import Control.Category
import Data.Profunctor
import Data.Profunctor.Strong
import Prelude hiding (id, (.))
One way...
newtype WrapP p a b = WrapP { unwrapP :: p a b }
instance Category p => Category (WrapP p) where
id = WrapP id
WrapP p . WrapP q = WrapP (p . q)
instance (Category p, Strong p) => Arrow (WrapP p) where
first = WrapP . first' . unwrapP
second = WrapP . second' . unwrapP
-- NB. the first usage of id comes from (->)'s Category instance (id :: a -> a)
-- but the second uses p's instance (id :: p a a)
arr f = WrapP $ dimap f id id
... and t'other...
newtype WrapA p a b = WrapA { unwrapA :: p a b }
instance Arrow p => Profunctor (WrapA p) where
dimap f g p = WrapA $ arr f >>> unwrapA p >>> arr g
instance Arrow p => Strong (WrapA p) where
first' = WrapA . first . unwrapA
second' = WrapA . second . unwrapA