65

I am learning functionnal programming with Haskell, and I try to grab concepts by first understanding why do I need them.

I would like to know the goal of arrows in functional programming languages. What problem do they solve? I checked http://en.wikibooks.org/wiki/Haskell/Understanding_arrows and http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf. All I understand is that they are used to describe graphs for computations, and that they allow easier point free style coding.

The article assume that point free style is generally easier to understand and to write. This seems quite subjective to me. In another article (http://en.wikibooks.org/wiki/Haskell/StephensArrowTutorial#Hangman:_Main_program), a hangman game is implemented, but I cannot see how arrows makes this implementation natural.

I could find a lot of papers describing the concept, but nothing about the motivation.

What I am missing?

4 Answers 4

46

I realize I'm coming late to the party, but you've had two theoretical answers here, and I wanted to provide a practical alternative to chew over. I'm coming at this as a relative Haskell noob who nonetheless has been recently force-marched through the subject of Arrows for a project I'm currently working on.

First, you can productively solve most problems in Haskell without reaching for Arrows. Some notable Haskellers genuinely do not like and do not use them (see here, here, and here for more on this). So if you're saying to yourself "Hey, I don't need these," understand that you may genuinely be correct.

What I found most frustrating about Arrows when I first learned them was how the tutorials on the subject inevitably reached for the analogy of circuitry. If you look at Arrow code -- the sugared variety, at least -- it resembles nothing so much as a Hardware Defnition Language. Your inputs line up on the right, your outputs on the left, and if you fail to wire them all up properly they simply fail to fire. I thought to myself: Really? Is this where we've ended up? Have we created a language so completely high-level that it once again consists of copper wires and solder?

The correct answer to this, as far as I've been able to determine, is: Actually, yes. The killer use case right now for Arrows is FRP (think Yampa, games, music, and reactive systems in general). The problem facing FRP is largely the same problem facing all other synchronous messaging systems: how to wire a continuous stream of inputs into a continuous stream of outputs without dropping relevant information or springing leaks. You can model the streams as lists -- several recent FRP systems use this approach -- but when you have a lot of inputs lists become almost impossible to manage. You need to insulate yourself from the current.

What Arrows allow in FRP systems is the composition of functions into a network while at the same time entirely abstracting away any reference at all to the underlying values being passed by those functions. If you're new to FP, this can be confusing at first, and then mind-blowing when you've absorbed the implications of it. You've only recently absorbed the idea that functions can be abstracted, and how to understand a list like [(*), (+), (-)] as being of type [(a -> a -> a)]. With Arrows, you can push the abstraction one layer further.

This additional ability to abstract carries with it its own dangers. For one thing, it can push GHC into corner cases where it doesn't know what to make of your type assumptions. You'll have to be prepared to think at the type level -- this is an excellent opportunity to learn about kinds and RankNTypes and other such topics.

There are also a number of examples of what I'd call "Stupid Arrow Stunts" where the coder reaches for some Arrow combinator just because he or she wants to show off a neat trick with tuples. (Here's my own trivial contribution to the madness.) Feel free to ignore such hot-dogging when you come across it in the wild.

NOTE: As I mentioned above, I'm a relative noob. If I've promulgated any misconceptions above, please feel free to correct me.

2
  • 2
    I am happy that I had not accepted anything yet. Thank you for providing this answer. It is more focused on users. The exemples are great. The subjective parts are clearly defined and balanced. I hope that people who have upvoted this question will come back and see this. Nov 10, 2011 at 11:26
  • While arrows are definitely the wrong tool for your linked solution, I feel like I need to mention that removeAt' n = arr(\ xs -> (xs,xs)) >>> arr (take (n-1)) *** arr (drop n) >>> arr (uncurry (++)) >>> returnA can be more concisely and clearly written as removeAt' n = (arr (take $ n-1) &&& arr (drop n)) >>> (arr $ uncurry (++)).
    – cemper93
    Nov 21, 2015 at 16:49
32

This is kind of a "soft" answer, and I'm not sure if any reference actually states it in this manner, but this is how I've come to think of arrows:

An arrow type A b c is basically a function b -> c but with more structure in the same way that a monadic value M a has more structure than a plain old a.

Now what that extra structure happens to be depends on the particular arrow instance you're talking about. Just as with monads IO a and Maybe a each have different additional structure.

The thing that you get with monads is an inability to go from an M a to an a. Now this may seem like a limitation, but it's actually a feature: the type system is protecting you from turning a monadic value into a plain old value. You can only make use of the value by participating in the monad via >>= or the primitive operations of the particular monad instance.

Likewise the thing that you get from A b c is an inability to construct a new b-consuming c-producing "function". The arrow is protecting you from consuming the b and creating a c except by participating in the various arrow combinators or by using the primitive operations of the particular arrow instance.

For example the signal functions in Yampa are roughly (Time -> a) -> (Time -> b), but additionally they have to obey a certain causality restriction: the output at time t is determined by the past values of the input signal: you can't look into the future. So what they do is instead of programming with (Time -> a) -> (Time -> b), you program with SF a b and you build your signal functions out of primitives. It so happens that since SF a b behaves a lot like a function, so that common structure is what's called an "arrow".

1
  • "The arrow is protecting you from consuming the b and creating a c except by participating in the various arrow combinators or by using the primitive operations of the particular arrow instance." With apologies for replying to this ancient answer: this sentence made me think of linear types, i.e. that resources can't be cloned or vanished. Do you think there might be any connection?
    – glaebhoerl
    Jul 14, 2014 at 21:24
15

I like to think of Arrows, like Monads and Functors, as allowing the programmer to do exotic compositions of functions.

Without Monads or Arrows (and Functors), composition of functions in a functional language is limited to applying one function to the result of another function. With monads and functors, you can define two functions, and then write separate reusable code which specify how those functions, in the context of the particular monad, interact with each other and with the data which is passed into them. This code is placed within the bind code of the Monad. So a monad is one one view, just a container for reusuable bind code. Functions compose differently within the context of one monad from another monad.

A simple example is the Maybe monad, where there is code in the bind function such that if a function A is composed with a function B within a Maybe monad, and B produces a Nothing, then the bind code will ensure that the composition of the two functions outputs a Nothing, without bothering to apply A to the Nothing value coming out from B. If there were no monad, the programmer would have to write code into A to test for a Nothing input.

Monads also mean that the programmer does not need to explicitly type the parameters which each function requires into the source code - the bind function handles parameter passing. So using monads, the source code can begin to look more like a static chain of function names, rather than looking as though function A "calls" function B with parameters C and D - the code starts to feel more like an electronic circuit than a moving machine - more functional than imperative.

Arrows also connect functions together with a bind function, providing reusable functionality and hiding parameters. But Arrows can themselves be connected together and composed, and can optionally route data to other Arrows at runtime. Now you can apply data to two paths of Arrows, which "do different things" to the data, and reassemble the result. Or you can select which branch of Arrows to pass the data to, depending on some value in the data. The resulting code is even more like an electronic circuit, with switches, delays, integration etc. The program looks very static, and you should not be able to see much manipulation of data going on. There are fewer and fewer parameters to think about, and less need to think about what values parameters may or may not take.

Writing an Arrowized program mostly involves selecting off the shelf Arrows such as splitters, switches, delays and integrators, lifting functions into those Arrows, and connecting the Arrows together to form bigger Arrows. In Arrowized Functional Reactive Programming, the Arrows form a loop, with input from the world being combined with output from the last iteration of the program, so that the output reacts to real world input.

One of the real world values is time. In Yampa, the Signal Function Arrow invisibly threads the time parameter through the computer program - you never access the time value, but if you connect an integrator arrow into the program, it will output values integrated over time which you can then use to pass to other arrows.

1
  • but this sounds like an applicative functor (some wrapper around a function, that provides helper functions, in some specific context, for reusing already existing functions for the wrapped-types). i definitely need to read more to understand, but maybe you can help, by pointing out what i am missing
    – Belun
    May 14, 2013 at 9:35
4

Just an addition to the other answers: Personally it helps me a lot to understand what such a concept is (mathematically) and how it relates to other concepts I know.

In the case of arrows, I found the following paper helpful - it compares monads, applicative functors (idioms) and arrows: Idioms are oblivious, arrows are meticulous, monads are promiscuous by Sam Lindley, Philip Wadler and Jeremy Yallop.

Also I believe nobody mentioned this link which may provide you some ideas and literature on the subject.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.