Search code examples
haskelltypeclassarrowsbifunctor

Bifunctor vs. Arrow methods


There is a bit of overlap between Bifunctor and Arrow methods:

class Bifunctor p where
  first :: (a -> a') -> p a b -> p a' b
  second :: (b -> b') -> p a b -> p a b'
  bimap :: (a -> a') -> (b -> b') -> p a b -> p a' b'

class Arrow (~~>) where
  ...
  first :: (a ~~> a') -> (a, b) ~~> (a', b)
  second :: (b ~~> b') -> (a, b) ~~> (a, b')
  (***) :: (a ~~> a') -> (b ~~> b') -> (a, b) ~~> (a', b')

The Bifunctor class comes with laws completely analogous to those of Functor.

The Arrow class comes with a number of laws different laws and a somewhat cryptic warning about (***): "Note that this is in general not a functor." Surprisingly (to me) there's only one law about (***):

first f >>> arr (id *** g) = arr (id *** g) >>> first f

The Arrow (->) instance and the Bifunctor (,) instance match up exactly, so that bimap @(,) = (***) @(->). Is there some special significance to this? Is there a meaningful hypothetical

class Foo (~~>) p where
  biFoo :: (a ~~> a') -> (b ~~> b') -> p a b ~~> p a' b'

If so, does that admit functional dependencies?


Solution

  • Arrow is a (somewhat bastardized) precursor to a class of cartesian closed categories, or a least cartesian monoidal categories. Specifically, to monoidal categories whose tensor product is (,) and unit element ().

    Recall that a monoidal category is characterised by the tensor product as a bifunctor, so there's your connection between Arrow and Bifunctor.

    *** has in fact more laws than you listed, only, the library chooses to formulate those in terms of first instead. Here's an equivalent definition of the class:

    class (Category k, Category k') => EnhancedCategory k k' where
      arr :: k a b -> k' a b
      -- arr id ≡ id
      -- arr (f . g) = arr f . arr g
    class (EnhancedCategory (->) a) => Arrow a where
      (***) :: a b c -> a b' c' -> a (b,b') (c,c')
      -- (f***id) . (g***id) ≡ (f.g)***id
      -- (id***f) . (id***g) ≡ id***(f.g)
      -- arr fst . (f***id) ≡ f . arr fst
      -- arr snd . (id***g) ≡ g . arr snd
      -- ¿ arr swap . (f***g) ≡ (g***f) . arr swap ?
      -- ((f***g)***h) . assoc ≡ assoc . (f***(g***h))
      diag :: a b (b,b)
    
    first :: Arrow a => a b c -> a (b,d) (c,d)
    first f = f***id
    second :: Arrow a => a b c -> a (d,b) (d,c)
    second g = id***g
    (&&&) :: Arrow a => a b c -> a b d -> a b (c,d)
    f&&&g = (f***g) . diag
    

    Incidentally, it is also possible to remove the arr for lifting pure functions, and instead give the superclass only the dedicated methods fst, snd and assoc. I call that class Cartesian. This allows defining “arrow” categories that don't contain arbitrary Haskell functions; linear maps are an important example.