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
= IOArrow $ return . f
arr f IOArrow f) = IOArrow $ \(a, c) -> do
first (<- f a
x return (x, c)
Then I make some simple functions I want to compose:
foo :: Int -> String
= show
foo
bar :: String -> IO Int
= return . read bar
And use them:
main :: IO ()
= do
main let f = arr (++ "!") . arr foo . IOArrow bar . arr id
<- runIOArrow f "123"
result 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 ()
= do
main let g = arr (++ "!") . arr foo . Kleisli bar . arr id
<- runKleisli g "123"
result 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
= proc x -> do
arrowUser f <- f -< x
y -< y
returnA
main :: IO ()
= do
main let h = arr (++ "!")
<<< arr foo
<<< Kleisli bar
<<< arr id
<- runKleisli (arrowUser h) "123"
result 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
= f x
arrowUser' f x
main' :: IO ()
= do
main' let h = (++ "!") . foo . unsafePerformIO . bar . id
= arrowUser' h "123"
result putStrLn result
Try writing that without unsafePerformIO
, and without arrowUser'
having to
deal with any Monad
type arguments.