I'm broadly familiar with the concepts of monads and arrows as used in functional programming. I also understand that they can be used to solve similar kinds of problems.
However, I'm still a bit confused about how to select which one to use in any given situation.
When should I use monads and when should I use arrows?
(->) is often called the "function arrow" or "function type constructor", and while it does have some special syntax, there's not that much special about it. It's essentially an infix type operator. Give it two types, and it gives you the type of functions between those types.
A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.
The Arrow (either (->) or MyArr ) is an abstraction of a computation. For a function b -> c , b is the input and c is the output. For a MyArr b c , b is the input and c is the output.
In functional programming, a monad is a software design pattern with a structure that combines program fragments (functions) and wraps their return values in a type with additional computation.
There are two excellent papers by Lindley, Wadler & Yallop (discussed at LTU here).
The most important thing to understand is that there are more things which are arrows than there are things which are monads. Conversely, monads are strictly more powerful than arrows (the second paper above specifies precisely in which fashion).
In particular, monads are arrows equipped with an apply function of type (a ~> b, a) ~> b
, where (~>)
is the constructor for a given arrow. Lindley et al. point out that this destroys the meticulous distinction arrows maintain between terms and commands (or, if you prefer, objects and morphisms).
Applicative functors have a wide variety of applications, particularly for things which are best thought of as operations on streams. One can in fact think of arrows as arising from generalizing the notion of a transformer on streams (i.e. introducing a new language for morphisms on objects constructed by a given applicative functor).
In my experience, because monads blur the distinction between objects and morphisms (i.e., if I'm using the words correctly, give rise to a closed cartesian category), then a term in a monad is generally far more necessarily opaque than a term in an arrow or applicative functor (although note that both let you inject arbitrary functions by the arr
and pure
methods respectively).
So if something is not given the characteristics of a monad (even though conceptually it forms one), then it is potentially open to greater inspection and optimization. It is also potentially easier to serialize. Hence the use of applicatives and arrows in parsers and circuit modeling.
The above attempted to be general and descriptive. Below are some of my opinionated rules of thumb.
If you need to model something that looks like state, start with a monad. If you need to model something that looks like global control flow (i.e. exceptions, continuations), start with a monad. If a requirement arises that conflicts with the power and generality of monads (i.e. for which join (join :: m (m a) -> m a)
is too powerful), then consider chipping away at the power of the thing you're using.
If you need to model streams, and transformations on streams, and particularly streams for which certain characteristics (particularly unlimited views of the past and future) should be opaque, then start with an applicative functor. If you need stronger reasoning about properties of transformations on streams, then think about reaching for an arrow.
Or, very crudely put, applicatives are for the actions of circuits, arrows are for the structures of circuits, and monads are for general-purpose computational effects.
There's of course much more to the story. For applicatives, see Conal Elliott's work on FRP in particular. For arrows, see the HXT XML parser library, the Yampa FRP project, the Haskell on a Horse web framework, Hudak and Liu's classic "Plugging a Space Leak with an Arrow" paper, among other things. For monads, see everywhere. And of course take note that just because something is a monad, that doesn't mean that applicative notation might not be clearer and more expressive.
The short answer is that Arrows are more general than Monads and are also more cumbersome to use. Thus you should use Monads whenever you can, leaving the use of Arrows for the cases where Monads are not applicable.
The “scenic route” answer follows.
John Hughes, the person who introduced Arrows, has published two great papers that I recommend: “Generalising monads to arrows” and “Programming with Arrows”. These two articles are easy to read and provide the answer to your question. Even if some people do not understand all the details or the code in these two articles, they will certainly find lots of information and very helpful explanations about Monads and Arrows.
I will now highlight the main points from these articles that pertain to your question
When Monads were introduced, people thought that they were all-powerful. Indeed, Monads pack a lot of power. But at some point, it was found that there are cases where Monads cannot be applied. These cases have to do with multiple inputs, especially when some of the inputs are static and some of the inputs are dynamic. So, John Hughes stepped up and introduced Arrows.
Arrows are more general than Monads. Arrows are a superset of Monads. They can do all that Monads do and more. But they are also more difficult to use. John Hughes recommends that you should use Monads whenever you can and that you should use Arrows when you cannot use Monads.
I agree with John Hughes. I am also reminded of Einstein’s quote “Everything should be made as simple as possible, but not simpler”.
Of course, it all depends on the particular situation at hand. Let me explain. Let us suppose that you are learning Haskell. Then it would be a great task to do each program using a monadic approach and redo it using an arrow-based approach. When you learn, you should strive to explore all possibilities and implement all kinds of approaches. This way, you obtain great insight and you are able to compare different solutions first-hand.
Now let us suppose that you want to provide a library to the community. Well, you owe it to the people that will read your code that you should use the approach that is easiest to understand and still gets the job done. You also owe it to the people that will use your code that your solution lacks unnecessary complexity. This way, your solution is more easily maintainable and less prone to errors and bugs.
But what if you are in a borderline case? Let us suppose that you are not sure whether or not you will need the extra power of Arrows. Then what should you do? Should you start with a monadic approach and later switch to an arrow-based one if the need arises? Or should you start with Arrows from the get-go, avoiding a costly switch halfway through the project?
Again, my answer is to try the first approach: try to use Monads if you can. If you later find out that you cannot use Monads, you will have to endure a costly switch, where you will have to restart and redo the project in order to use Arrows. This approach will certainly require more time and other resources from your part. But you will know that you did the correct thing, which was to try to provide the simplest, clearest, less complex solution possible.
Avoiding unnecessary complexity is the most important thing. Believe it or not, this is the reason concepts (such as function composition, Monads and Arrows) from Category Theory were introduced to Computer Science. Ironic?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With