“Programming with Arrows”,
John Hughes, in 5th International Summer School on
Advanced Functional Programming,
Lecture Notes in Computer Science vol. 3622,
Springer, 2004.
This paper includes another introduction to the notation,
with practical examples.

With the -XArrows flag, GHC supports the arrow
notation described in the second of these papers,
translating it using combinators from the
Control.Arrow
module.
What follows is a brief introduction to the notation;
it won't make much sense unless you've read Hughes's paper.

The extension adds a new kind of expression for defining arrows:

exp10 ::= ...
| proc apat -> cmd

where proc is a new keyword.
The variables of the pattern are bound in the body of the
proc-expression,
which is a new sort of thing called a command.
The syntax of commands is as follows:

cmd ::= exp10 -< exp
| exp10 -<< exp
| cmd0

with cmd0 up to
cmd9 defined using
infix operators as for expressions, and

where calts are like alts
except that the bodies are commands instead of expressions.

Commands produce values, but (like monadic computations)
may yield more than one value,
or none, and may do other things as well.
For the most part, familiarity with monadic notation is a good guide to
using commands.
However the values of expressions, even monadic ones,
are determined by the values of the variables they contain;
this is not necessarily the case for commands.

A simple example of the new notation is the expression

proc x -> f -< x+1

We call this a procedure or
arrow abstraction.
As with a lambda expression, the variable x
is a new variable bound within the proc-expression.
It refers to the input to the arrow.
In the above example, -< is not an identifier but an
new reserved symbol used for building commands from an expression of arrow
type and an expression to be fed as input to that arrow.
(The weird look will make more sense later.)
It may be read as analogue of application for arrows.
The above example is equivalent to the Haskell expression

arr (\ x -> x+1) >>> f

That would make no sense if the expression to the left of
-< involves the bound variable x.
More generally, the expression to the left of -<
may not involve any local variable,
i.e. a variable bound in the current arrow abstraction.
For such a situation there is a variant -<<, as in

proc x -> f x -<< x+1

which is equivalent to

arr (\ x -> (f x, x+1)) >>> app

so in this case the arrow must belong to the ArrowApply
class.
Such an arrow is equivalent to a monad, so if you're using this form
you may find a monadic formulation more convenient.

7.10.1. do-notation for commands

Another form of command is a form of do-notation.
For example, you can write

You can read this much like ordinary do-notation,
but with commands in place of monadic expressions.
The first line sends the value of x+1 as an input to
the arrow f, and matches its output against
y.
In the next line, the output is discarded.
The arrow returnA is defined in the
Control.Arrow
module as arr id.
The above example is treated as an abbreviation for

The syntax is the same as for case expressions,
except that the bodies of the alternatives are commands rather than expressions.
The translation is similar to that of if commands.

7.10.3. Defining your own control structures

As we're seen, arrow notation provides constructs,
modelled on those for expressions,
for sequencing, value recursion and conditionals.
But suitable combinators,
which you can define in ordinary Haskell,
may also be used to build new commands out of existing ones.
The basic idea is that a command defines an arrow from environments to values.
These environments assign values to the free local variables of the command.
Thus combinators that produce arrows from arrows
may also be used to build commands from commands.
For example, the ArrowChoice class includes a combinator

It is essential that this operator be polymorphic in e
(representing the environment input to the command
and thence to its subcommands)
and satisfy the corresponding naturality property

arr k >>> (f <+> g) = (arr k >>> f) <+> (arr k >>> g)

at least for strict k.
(This should be automatic if you're not using seq.)
This ensures that environments seen by the subcommands are environments
of the whole command,
and also allows the translation to safely trim these environments.
The operator must also not use any variable defined within the current
arrow abstraction.

7.10.4. Primitive constructs

Some operators will need to pass additional inputs to their subcommands.
For example, in an arrow type supporting exceptions,
the operator that attaches an exception handler will wish to pass the
exception that occurred to the handler.
Such an operator might have a type

handleA :: ... => a e c -> a (e,Ex) c -> a e c

where Ex is the type of exceptions handled.
You could then use this with arrow notation by writing a command

body `handleA` \ ex -> handler

so that if an exception is raised in the command body,
the variable ex is bound to the value of the exception
and the command handler,
which typically refers to ex, is entered.
Though the syntax here looks like a functional lambda,
we are talking about commands, and something different is going on.
The input to the arrow represented by a command consists of values for
the free local variables in the command, plus a stack of anonymous values.
In all the prior examples, this stack was empty.
In the second argument to handleA,
this stack consists of one value, the value of the exception.
The command form of lambda merely gives this value a name.

More concretely,
the values on the stack are paired to the right of the environment.
So operators like handleA that pass
extra inputs to their subcommands can be designed for use with the notation
by pairing the values with the environment in this way.
More precisely, the type of each argument of the operator (and its result)
should have the form

a (...(e,t1), ... tn) t

where e is a polymorphic variable
(representing the environment)
and ti are the types of the values on the stack,
with t1 being the “top”.
The polymorphic variable e must not occur in
a, ti or
t.
However the arrows involved need not be the same.
Here are some more examples of suitable operators:

We can supply the extra input required by commands built with the last two
by applying them to ordinary expressions, as in

proc x -> do
s <- ...
(|runReader (do { ... })|) s

which adds s to the stack of inputs to the command
built using runReader.

The command versions of lambda abstraction and application are analogous to
the expression versions.
In particular, the beta and eta rules describe equivalences of commands.
These three features (operators, lambda abstraction and application)
are the core of the notation; everything else can be built using them,
though the results would be somewhat clumsy.
For example, we could simulate do-notation by defining

7.10.5. Differences with the paper

Instead of a single form of arrow application (arrow tail) with two
translations, the implementation provides two forms
“-<” (first-order)
and “-<<” (higher-order).

User-defined operators are flagged with banana brackets instead of
a new form keyword.

7.10.6. Portability

Although only GHC implements arrow notation directly,
there is also a preprocessor
(available from the
arrows web page)
that translates arrow notation into Haskell 98
for use with other Haskell systems.
You would still want to check arrow programs with GHC;
tracing type errors in the preprocessor output is not easy.
Modules intended for both GHC and the preprocessor must observe some
additional restrictions: