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:
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.