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).