Brandon.Si(mmons)

code / art / projects

Categories That Want to Be Arrows

This is me recording some half-formed thoughts that have been rattling around in my head for the last week. They concern haskell’s Arrow typeclass; in particular it’s frustrating inability to be applied to types that are in Category but have no suitable implementation for arr.

Consider something as simple as:

newtype Bij a b = Bij (a -> b) (b -> a)

a Category instance is trivial, but an Arrow instance is impossible, since we would need to supply a b -> a given only a a -> b.

And yet most of the Arrow methods (except arr and &&&) would be useful for, and can be implemented in a very reasonable way, for the type above:

(Bij bc cb) *** (Bij xy yx) = Bij (bc *** xy) (cb *** yx)
first b                     = b *** id
second b                    = id *** b

We omit &&& above because there isn’t a reasonable way to define a general function :: (c -> b) -> (c' -> b) -> ((c,c') -> b

(Bij bc cb) &&& (Bij bc' c'b) = Bij (bc &&& bc') undefined

…without something like a “restricted Arrow” where b is constrained to Monoid. (but I’m getting ahead of myself; more on why I don’t think &&& belongs in Arrow anyway below).

Or as a reductio ad absurdum, consider simply the following type:

newtype Flipped a b = Flipped (b -> a)

is there any reason we shouldn’t have an Arrow-like class for which the above can be an instance?

None of this is new; it is a common complaint about Arrow: that it is overly complex (to the point of feeling arbitrary), while being extremely limited.

Won’t someone think of the Lenses?

Anyone who has tried to write a lens library has wished they could use the Arrow abstraction and slammed their head into a wall for at least an hour before giving up (except probably Edward Kmett). They just really ought to be Arrows.

Consider this lens representation:

newtype Lens a b = Lens (a -> (b -> a, b))

&&& will give us the same problems as the (admittedly very similar) Bij example above, but the other methods (***),first and second have good implementations that would even preserve the Lens Laws (they are what you would expect, but also have some foundations in CT)

An ArrowChoice implementation should also be no problem to implement while also being practically useful for lenses.

Again though, arr presents a problem. One might even try to implement it as a getter combined with a const/“read-only” setter continuation like:

arr f = Lens $ \a-> (const a, f a)

But this violates the first Arrow law arr id = id.

What is the nature of the problem?

From the above, it is obvious that we have problems with types where the flow of the computation is reversed from the order of the type variables. We can also probably find types that are sort of composite categories with a natural Category instance but no way to lift a pure computation.

But here’s another slightly different type of example; a computation with an associated cost, perhaps defining the “energy required” for a certain transformation:

data Transformation a b = T Int (a -> b)

instance Category Transformation where
    id = T 0 id
    (T n f) . (T n' g) = T (n+n') (f . g)

Again, this type lends itself well to the parallel composition operators in Arrow. For instance &&& can capture that composing two transformations into a single one that “fans out” carries the cost of the sum of their energy requirements:

(T n f) &&& (T n' g) = T (n+n') (f  g)

But the only logical implementation for arr that we can define, arr f = T 0 f, breaks the law arr (f >>> g) = arr f >>> arr g.

I started to do a survey of types on hackage that are Category but not Arrow, but got burnt out at the stage where I had to actually look through the code. Still this would be a great thing to do.

Default implementations without arr

If we rip arr out of the Arrow class, we lose a lot of the default methods, which are implemented in terms of some primitive “identity-like” functions lifted to Arrow with arr. Here are those functions from the main class:

-- second in terms of first, and vice versa:
\(x,y) -> (y,x)
-- &&& in terms of ***:
\b -> (b,b)

and these are the helpers from ArrowChoice:

-- right in terms of left:
either Right Left
-- ||| in terms of +++:
either id id

It’s useful to try to think of implementing these primitive helpers in a given Category, e.g. for some Category c can we implement swap :: c (x,y) (y,x)? And for any Category where we can define swap does it follow that we should always be able to define the corresponding ArrowChoice helper swapE :: c (Either a b) (Either b a)? If so, then they should be in the same new sub-class of Category, etc., etc.

I think these primitive helpers would probably be a good basis for the methods in a new class or set of classes to act as an Arrow alternative.

what about all those laws?

Most of the invariants that should hold for an Arrow are defined in terms of arr, but I think there are probably reasonable replacements, especially since there was never anything particularly rigorous about the original Arrow laws.

For example, here are some relationships which I think should be expected to hold:

  • first id = second id = id
  • f *** g = first f >>> second g (straight from default implementation)

but I’m not very good at this.

I suspect arr is either the anchor that allows for a well-defined Arrow class, or that all the laws are necessary to deal with the inclusion of arr. I hope the latter, and that a revised set of laws for Arrow-without-arr would look simpler and more abstract, like Monoid.

About &&&

While an arrow that can “fan out” is useful, this doesn’t belong in this class for a number of reasons:

  1. It doesn’t depend on Category at all and doesn’t even require a type of kind * -> * -> *; since any Arrow can be made an Applicative anyway, this could be re-written as (&&&) :: Applicative a=> a c -> a c' -> a (c,c')

  2. it appears only once in the entire implementation (in the Arrow class declaration). The Arrow laws don’t require it, nor do any of the default class implementations use it.

  3. because there are many Arrow-like things which have no suitable implementation for such a function (a bit of circular logic, I know)

Concluding remarks

Arrow fails as a generally-useful and coherent class for composing Categories vertically or “in parallel”, which is I think what people are looking for.

Maybe I can come up with some code soon for a better Arrow. Such a set of classes would hopefully be simpler, more flexible, and take advantage of the Arrow = Category + Applicative correspondence.

Also as I was finishing this I just stumbled on Causal Commutative Arrows (PDF) which look interesting.

Let me know your thoughts.

Update 2/13

Thanks for all the feedback everyone. From the comments I was pointed to Adam Megacz’s work on “generalized arrows”, and there are also a few comments on the haskell subreddit.

Comments