Идиоматический способ обработки вложенного ввода-вывода в Haskell
Я изучаю Haskell и пишу короткую парсинг script как упражнение. Большинство моих script состоят из чистых функций, но у меня есть два вложенных компонента IO:
- Прочитайте список файлов с пути.
- Прочитайте содержимое каждого файла, который, в свою очередь, будет использоваться для большей части остальной части программы.
У меня есть работы, но вложенные IO и слои fmap "чувствуют" неуклюжими, например, я должен либо избегать вложенного ввода-вывода (как-то), либо более умело использовать обозначения, чтобы избежать всех fmaps. Мне интересно, если я слишком усложняю ситуацию, делаю это неправильно и т.д. Вот какой-то соответствующий код:
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
В итоге я столкнулся с IO [IO String]
с помощью listDirectory в качестве входного для readFile. Не неуправляемый, но если я использую обозначение, чтобы развернуть [IO String]
для отправки какой-либо функции парсера, я все равно в конечном итоге либо использую вложенный fmap
, либо загрязняю мои якобы чистые функции с помощью IO-осведомленности (fmap и т.д.). Последнее кажется хуже, поэтому я делаю первое. Пример:
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... Я чувствую, что я непреднамеренно сделал это более сложным, чем это необходимо, и не могу представить, насколько это было бы запутанно, если бы я имел более глубокое вложение IO.
Предложения?
Спасибо заранее.
Ответы
Ответ 1
Я думаю, вам нужно что-то подобное для вашего main
:
main = do
paths <- getPaths "./configs"
flatConfigs <- traverse getConfig paths
let blockConfigs = fmap chunk flatConfigs
-- Parse and do stuff with config data.
return ()
Сравнить
fmap :: Functor f => (a -> b) -> f a -> f b
и
traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
Они очень похожи, но traverse
позволяет использовать такие эффекты, как IO
.
Вот типы, которые еще раз специализировались для сравнения:
fmap :: (a -> b) -> [a] -> [b]
traverse :: (a -> IO b) -> [a] -> IO [b]
(traverse
также известен как mapM
)
Ответ 2
Ваша идея "вложенности" на самом деле довольно хорошее представление о том, что такое монады. Монады можно рассматривать как Функторы с двумя дополнительными операциями, возвращать с типом a -> m a
и присоединяться к типу m (m a) -> m a
. Затем мы можем выполнять функции типа a -> m b
:
fmap :: (a -> m b) -> m a -> m (m b)
f =<< v = join (fmap f v) :: (a -> m b) -> m a -> m b
Итак, мы хотим использовать соединение здесь, но в настоящее время m [m a]
, поэтому наши комбинаторы монады не помогут напрямую. Позволяет искать m [m a] -> m (m [a])
с помощью hoogle, и наш первый результат выглядит многообещающим. Это sequence:: [m a] -> m [a]
.
Если мы посмотрим на связанную функцию, мы также найдем traverse :: (a -> IO b) -> [a] -> IO [b]
, которая аналогично sequence (fmap f v)
.
Вооружившись этим знанием, мы можем просто написать:
readConfigFiles path = traverse getConfig =<< getPaths path