8
votes

I learned and searched about Arrows for a while, and I'm a bit confused about necessity of Arrow class. As I know, Arrow class is abstraction of function, and Arrow A a b c represents something takes input of type b and output of type c. Also, it provides several fundamental operations like >>>, arr, and first.

However, I can't find any difference between standard function of type b -> c and Arrow of type A a b c. In my view, first and >>> can be replaced by \(b, c) -> (f b, c), and (.). Also, since every computation inside arrow are represented by function, if we replace arrows by those function, I think there will be no difference.

In short, I think every node of computation graph of Arrows(something like in https://www.haskell.org/arrows/syntax.html) can be replaced by standard function of Haskell. If it is true, why do we use Arrow instead of functions?

2

2 Answers

6
votes

Having an abstraction that obey certain laws allows you to do generic programming. You could program without any type-classes (no monads, applicatives, no equality/ordering etc.), but this would be quite inconvenient as you won't be able to write generic code that takes advantage of these properties.

Just as if you say you don't want the Ord instance, and then you'd have to rewrite the implementation of Set separately for every data type that you can order.

The point of Arrow is to describe computations that

  • take input,
  • produce output, and
  • have certain in the middle (effect is a colloquial expression).

So an arrow A b c is not a function a -> b. Most likely an arrow is implemented internally as a function, but a more complex one, and the point of implementing the Arrow interface is to describe (among other things) how they compose.

Specifically for arrows, you have the arrow notation that allows you to use the same notation for any valid Arrow. To give an example, in the netwire package the Wire data type implements Arrow, so you can use the arrow notation, as well as all utility functions that work on arrows. Without the instance, you'd have to either have some netwire-specific syntax, or just use the function that netwire provides.

To give an example: The arrow corresponding to the State monad is a -> s -> (b, s). But two such functions don't compose using (.). You need to describe their composition, and that's exactly what Arrow does.


Update: There can be various notions of composability, but I guess you mean function-like composition. Yes, this composability comes from Arrow's Category superclass, which defines identity and composition.

The other part of Arrow comes from Strong profunctor as I've recently learned from What's the relationship between profunctors and arrows? (although this is not captured in the type-class hierarchy, as the Profunctor typeclass is younger than Arrow). Profunctors allow to be modified by pure computations from "both sides", see lmap/rmap/dimap in Profunctor. For arrows we have (<<^) and (^>>), which are expressed using arr and >>> by composing bv an arrow from either side with a pure arrow constructed using arr.

Finally arrows have strength wrt (,), which is captured by first :: Arrow a => a b c -> a (b, d) (c, d). This means that we can use an arrow only on a part of the input, passing another unchanged. This allows to construct "circuits" with "parallel wires" - without first it wouldn't be possible to save an output of one part of the computation and to use it somewhere further later on.

A good exercise is to draw a circuit that represent a computation and then trying to express it using Arrow primitives/utilities, or alternatively with the arrow syntax notation. You'll see that first (or ***) is essential for this.

See Arrows can multitask for nice drawings of the operations.

For more theoretical background Arrows are Strong Monads might be interesting (I haven't read it yet).

5
votes

That's because you are only looking at the (->) instance of Arrow. Other types can also declare instances of Arrow, in which the operations are more complicated. For example:

instance Monad m => Arrow (Kleisli m) where
    arr f = Kleisli (return . f)
    first (Kleisli f) = Kleisli (\ ~(b,d) -> f b >>= \c -> return (c,d))
    second (Kleisli f) = Kleisli (\ ~(d,b) -> f b >>= \c -> return (d,c))