Lost in Technopolis

by John Wiegley

Arrows are simpler than they appear

Posted by John Wiegley on October 20, 2012 with labels:

I have found that arrows in Haskell are far simpler than they might appear based on the literature. They are simply abstractions of functions.

To see how this is practically useful, consider that you have a bunch of functions you want to compose, where some of them are pure and some are monadic. For example, f :: a -> b, g :: b -> m1 c, and h :: c -> m2 d.

Knowing each of the types involved, I could build a composition by hand, but the output type of the composition would have to reflect the intermediate monad types (in the above case, m1 (m2 d)). What if I just wanted to treat the functions as if they were just a -> b, b -> c, and c -> d? That is, I want to abstract away the presence of monads and reason only about the underlying types. I can use arrows to do exactly this.

Here is an arrow which abstracts away the presence of IO for functions in the IO monad, such that I can compose them with pure functions without the user needing to know that IO is involved. We start by defining an IOArrow to wrap IO functions:

data IOArrow a b = IOArrow { runIOArrow :: a -> IO b }

instance Category IOArrow where
  id = IOArrow return
  IOArrow f . IOArrow g = IOArrow $ f <=< g

instance Arrow IOArrow where
  arr f = IOArrow $ return . f
  first (IOArrow f) = IOArrow $ \(a, c) -> do
    x <- f a
    return (x, c)

Then I make some simple functions I want to compose:

foo :: Int -> String
foo = show

bar :: String -> IO Int
bar = return . read

And use them:

main :: IO ()
main = do
  let f = arr (++ "!") . arr foo . IOArrow bar . arr id
  result <- runIOArrow f "123"
  putStrLn result

Here I am calling IOArrow and runIOArrow, but if I were passing these arrows around in a library of polymorphic functions, they would only need to accept arguments of type “Arrow a => a b c”. None of the library code would need to be made aware that a monad was involved. Only the creator and end user of the arrow needs to know.

Generalizing IOArrow to work for functions in any Monad is called the “Kleisli arrow”, and there is already a built-in arrow for doing just that:

main :: IO ()
main = do
  let g = arr (++ "!") . arr foo . Kleisli bar . arr id
  result <- runKleisli g "123"
  putStrLn result

You could of course also use arrow composition operators, and proc syntax, to make it a little clearer that arrows are involved:

arrowUser :: Arrow a => a String String -> a String String
arrowUser f = proc x -> do
  y <- f -< x
  returnA -< y

main :: IO ()
main = do
  let h =     arr (++ "!")
          <<< arr foo
          <<< Kleisli bar
          <<< arr id
  result <- runKleisli (arrowUser h) "123"
  putStrLn result

Here it should be clear that although main knows the IO monad is involved, arrowUser does not. There would be no way of “hiding” IO from arrowUser without arrows – not without resorting to unsafePerformIO to turn the intermediate monadic value back into a pure one (and thus losing that context forever). For example:

arrowUser' :: (String -> String) -> String -> String
arrowUser' f x = f x

main' :: IO ()
main' = do
  let h      = (++ "!") . foo . unsafePerformIO . bar . id
      result = arrowUser' h "123"
  putStrLn result

Try writing that without unsafePerformIO, and without arrowUser' having to deal with any Monad type arguments.


Powered by Hakyll.
Site source on github.