`Control.Arrow`

), with no rigour at all. All you need is a basic understanding of Haskell, e.g. functors and monads. Without even explaining what arrows are, let's first dive in with the triple ampersand operator

`(&&&)`

. Imagine two actions:`logNumber :: Double -> IO ()`

logNumber = print

`-- Multiples a number by a random fraction in the interval [0, 1)`

scaledownRandomly :: Double -> IO Double

scaledownRandomly n = do

fraction <- (randomIO :: IO Double)

return (n * fraction)

-- or, scaledownRandomly n = (*) <$> randomIO :: IO Double <*> pure n

You have a number, and you want to both record and scale the number. Easy, right?

`logAndScale :: Double -> IO Double`

logAndScale n = logNumber n >> scaledownRandomly n

Here's how to write it using

`(&&&)`

.`logAndScale :: Double -> IO Double`

logAndScale = uncurry (>>) <$> (logNumber &&& scaledownRandomly)

What

`(&&&)`

does is that given two **computations**and an input, it feeds the input to both computations. In this case, the resulting type after combining the two actions with

`(&&&)`

is `(m (), m Double)`

. Check the type of `(&&&)`

:`(&&&) :: Arrow a => a b c -> a b c' -> a b (c, c')`

In this case, a is actually

`(->)`

, which is an instance of Arrow. So, for this operator:the first argument is

`(->) Double (IO ())`

,the second argument is

`(->) Double (IO Double)`

, andthe result is

`(->) Double (IO (), IO Double)`

.Writing

`(->)`

in front is just a fancy infix way to denote function application. Applying (>>) sequentially composes the two actions in `(IO (), IO Double)`

, to give the ultimate result `(IO Double)`

.Let's try another operator from Control.Arrow, the (>>>) operator. This one joins two computations. Here's how to use it:

`logAndScale :: Double -> IO Double`

logAndScale = (logNumber &&& scaledownRandomly) >>> uncurry (>>)

In this case, the second computation is

`uncurry (>>)`

. It takes "two" inputs, coming from the previous computation. `(>>>)`

joins them. Writing it this way probably makes our intention clearer (i.e. sequentially compose the results at the end of the computation), instead of the previous example with `<$>`

in front. In fact I can see something like a pipeline now.The last operator today is the triple asterisk

`(***)`

. This operator takes two computations and two inputs, and applies the computation to each respectively. Check it out:`logFirstAndScaleSecond :: (Double, Double) -> IO Double`

logFirstAndScaleSecond = (f *** g) >>> uncurry (>>)

For example,

`logFirstAndScaleSecond (123, 100)`

will print `123`

, then return, say, `56.78`

.We're done! In the examples above we have been using the

`(->)`

instances of arrows, and saw how we can write the same things in different ways (point free). Arrows are just an abstraction of the notion of "computation" that takes something as its input and gives something as its output - nothing to be scared of at all! Hopefully this post has given you some intuition on the topic, enough footing to explore the library on your own.Useful references (you've probably read all of them):

I think having compilable code that someone can copy-paste and run is very useful, in addition to showing the types of the intermediate types, and GHCi output. You use f and g in function logFirstAndScaleSecond that don't exist. I fixed these problems in my notes here: https://blogs.asarkar.com/assets/docs/haskell/Arrow.md

ReplyDelete