Programming with Arrows [chapter]

John Hughes
2005 Lecture Notes in Computer Science  
Point-free programming Consider this simple Haskell definition, of a function which counts the number of occurrences of a given word w in a string: count w = length . filter (==w) . words This is an example of "point-free" programming style, where we build a function by composing others, and make heavy use of higher-order functions such as filter. Point-free programming is rightly popular: used appropriately, it makes for concise and readable definitions, which are well suited to equational
more » ... oning in the style of Bird and Meertens [2]. It's also a natural way to assemble programs from components, and closely related to connecting programs via pipes in the UNIX shell. Now suppose we want to modify count so that it counts the number of occurrences of a word in a file, rather than in a string, and moreover prints the result. Following the point-free style, we might try to rewrite it as count w = print . length . filter (==w) . words . readFile But this is rejected by the Haskell type-checker! The problem is that readFile and print have side-effects, and thus their types involve the IO monad: readFile :: String -> IO String print :: Show a => a -> IO () Of course, it is one of the advantages of Haskell that the type-checker can distinguish expressions with side effects from those without, but in this case we pay a price. These functions simply have the wrong types to compose with the others in a point-free style. Now, we can write a point-free definition of this function using combinators from the standard Monad library. It becomes: ) . liftM (length . filter (==w) . words) . readFile But this is no longer really perspicuous. Let us see if we can do better. In Haskell, functions with side-effects have types of the form a -> IO b. Let us introduce a type synonym for this: type Kleisli m a b = a -> m b So now we can write the types of readFile and print as readFile :: Kleisli IO String String print :: Show a => Kleisli IO a () We parameterise Kleisli over the IO monad because the same idea can be used with any other one, and we call this type Kleisli because functions with this type are arrows in the Kleisli category of the monad m. Now, given two such functions, one from a to b, and one from b to c, we can "compose" them into a Kleisli arrow from a to c, combining their side effects in sequence. Let us define a variant composition operator to do so. We choose to define "reverse composition", which takes its arguments in the opposite order to (.), so that the order in which we write the arrows in a composition corresponds to the order in which their side effects occur. (>>>) :: Monad m => Kleisli m a b -> Kleisli m b c -> Kleisli m a c (f >>> g) a = do b <-f a g b We can use this composition operator to define functions with side-effects in a point-free style -for example, the following function to print the contents of a file: printFile = readFile >>> print Returning to our original example, we cannot yet reprogram it in terms of (>>>) because it also involves functions without side-effects, and these have the wrong type to be composed by (>>>). Fortunately, we can easily convert a pure function of type a -> b into a Kleisli arrow with no side-effects. We define a combinator to do so, and we call it arr. arr :: Monad m => (a->b) -> Kleisli m a b arr f = return . f Using this combinator, we can now combine side-effecting and pure functions in the same point-free definition, and solve our original problem in the following rather clear way: count w = readFile >>> arr words >>> arr (filter (==w)) >>> arr length >>> print
doi:10.1007/11546382_2 fatcat:tsdjqcq4rvdmplgkbk4hoqkm3u