Sunday 11 December 2016

How to use Haskell arrows (for beginners)

This post is an introduction to Haskell arrows (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), and
the 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):
  1. Haskell/Understanding arrows
  2. Arrow tutorial - HaskellWiki
  3. Haskell/Arrow tutorial
  4. Haskell/Arrows
  5. Arrows: A General Interface to Computation

1 comment:

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