Search code examples
haskelliomonadsfunction-compositionio-monad

Function composition in the IO monad


The lines function in Haskell separates the lines of a string into a string list:

lines :: String -> [String]

The readFile function reads a file into a string:

readFile :: FilePath -> IO String

Trying to compose these functions to get a list of lines in a file results in a type error:

Prelude> (lines . readFile) "quux.txt"
<interactive>:26:10: error:
    • Couldn't match type ‘IO String’ with ‘[Char]’
      Expected type: FilePath -> String
        Actual type: FilePath -> IO String
    • In the second argument of ‘(.)’, namely ‘readFile’
      In the expression: lines . readFile
      In the expression: (lines . readFile) "quux.txt"

How can I do the monad trick here?


Solution

  • You can't compose them, at least not with (.) alone. You can use fmap (or its operator version <$>), though:

    lines <$> readFile "quux.txt"  -- Produces IO [String], not [String]
    

    One way to express this in terms of a kind of composition is to first create a Kleisli arrow (a function of type a -> m b for some monad m) from lines:

    -- return . lines itself has type Monad m => String -> m [String]
    -- but for our use case we can restrict the type to the monad
    -- we are actually interested in.
    kleisliLines :: String -> IO [String]
    kleisliLines = return . lines
    

    Now you can use the Kleisli composition operator >=> to combine readFile (itself a Kleisli arrow) and lines:

    import Control.Monad  -- where (>=>) is defined
    
    -- (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
    -- Here, m ~ IO
    --       a -> FilePath
    --       b -> String
    --       c -> [String]
    (readFile >=> kleisliLines) "quux.txt"
    

    Compare this with the >>= operator, which requires you to supply the file name to readFile before feeding the result to return . lines:

    -- m >>= return . f === fmap f m === f <$> m
    readFile "quux.txt" >>= kleisliLines
    

    >=> is natural if you are already thinking of a pipeline in terms of >=; if you want something that preserves the order of ., use <=< (also defined in Control.Monad, as (<=<) = flip (>=>); the operands are simply reversed).

    (kleisliLines <=< readFile) "quux.txt"