简体   繁体   中英

Idiomatic way to handle nested IO in 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.

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 )

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 -> ma and join with type m (ma) -> ma . We can then make functions of type a -> mb 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 [ma] at the moment so our monad combinators won't help directly. Lets search for m [ma] -> m (m [a]) using hoogle and our first result looks promising. It is sequence:: [ma] -> m [a] .
If we look at the related function we also find traverse :: (a -> IO b) -> [a] -> IO [b] which is similarly sequence (fmap fv) .

Armed with that knowledge we can just write:

readConfigFiles path = traverse getConfig =<< getPaths path

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM