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?
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"