Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to handle nested IO in Haskell

Tags:

haskell

I'm learning Haskell, and writing a short parsing script as an exercise. Most of my script consists of pure functions, but I have two, nested IO components:

  1. Read a list of files from a path.
  2. Read the contents of each file, which, in turn, will be the input for most of the rest of the program.

What I have works, but the nested IO and layers of fmap "feel" clunky, like I should either be avoiding nested IO (somehow), or more skillfully using do notation to avoid all the fmaps. I'm wondering if I'm over-complicating things, doing it wrong, etc. Here's some relevant code:

getPaths :: FilePath -> IO [String]
getPaths folder = do
    allFiles <- listDirectory folder
    let txtFiles = filter (isInfixOf ".txt") allFiles
        paths = map ((folder ++ "/") ++) txtFiles
    return paths

getConfig :: FilePath -> IO [String]
getConfig path = do
    config <- readFile path
    return $ lines config

main = do
    paths = getPaths "./configs"
    let flatConfigs = map getConfigs paths
        blockConfigs = map (fmap chunk) flatConfigs
    -- Parse and do stuff with config data.
    return

I end up dealing with IO [IO String] from using listDirectory as input for readFile. Not unmanageable, but if I use do notation to unwrap the [IO String] to send to some parser function, I still end up either using nested fmap or pollute my supposedly pure functions with IO awareness (fmap, etc). The latter seems worse, so I'm doing the former. Example:

type Block = [String]
getTrunkBlocks :: [Block] -> [Block]
getTrunkBlocks = filter (liftM2 (&&) isInterface isMatchingInt)
    where isMatchingInt line = isJust $ find predicate line
          predicate = isInfixOf "switchport mode trunk"

main = do
    paths <- getPaths "./configs"
    let flatConfigs = map getConfig paths
        blockConfigs = map (fmap chunk) flatConfigs
        trunks = fmap (fmap getTrunkBlocks) blockConfigs
    return $ "Trunk count: " ++ show (length trunks)

fmap, fmap, fmap... I feel like I've inadvertently made this more complicated than necessary, and can't imagine how convoluted this could get if I had deeper IO nesting.

Suggestions?

Thanks in advance.

like image 625
Nathan Hemingway Avatar asked Mar 02 '17 22:03

Nathan Hemingway


People also ask

Why is IO a Monad in Haskell?

The I/O monad contains primitives which build composite actions, a process similar to joining statements in sequential order using `;' in other languages. Thus the monad serves as the glue which binds together the actions in a program.

What is IO action in Haskell?

IO actions are used to affect the world outside of the program. Actions take no arguments but have a result value. Actions are inert until run. Only one IO action in a Haskell program is run ( main ). Do-blocks combine multiple actions together into a single action.


2 Answers

I think you want something like this for your main:

main = do
    paths <- getPaths "./configs"
    flatConfigs <- traverse getConfig paths
    let blockConfigs = fmap chunk flatConfigs
    -- Parse and do stuff with config data.
    return ()

Compare

fmap :: Functor f => (a -> b) -> f a -> f b

and

traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)

They are quite similar, but traverse lets you use effects like IO.

Here are the types again specialized a little for comparison:

fmap     :: (a -> b)    -> [a] -> [b]
traverse :: (a -> IO b) -> [a] -> IO [b]

(traverse is also known as mapM)

like image 172
user2297560 Avatar answered Oct 17 '22 22:10

user2297560


Your idea of 'nestedness' is actually a pretty good insight into what monads are. Monads can be seen as Functors with two additional operations, return with type a -> m a and join with type m (m a) -> m a. We can then make functions of type a -> m b composable:

fmap :: (a -> m b) -> m a -> m (m b)
f =<< v = join (fmap f v) :: (a -> m b) -> m a -> m b

So we want to use join here but have m [m a] at the moment so our monad combinators won't help directly. Lets search for m [m a] -> m (m [a]) using hoogle and our first result looks promising. It is sequence:: [m a] -> m [a].
If we look at the related function we also find traverse :: (a -> IO b) -> [a] -> IO [b] which is similarly sequence (fmap f v).

Armed with that knowledge we can just write:

readConfigFiles path = traverse getConfig =<< getPaths path
like image 34
Taren Avatar answered Oct 17 '22 22:10

Taren