【问题标题】:Idiomatic way to handle nested IO in Haskell在 Haskell 中处理嵌套 IO 的惯用方式
【发布时间】:2017-03-03 08:18:37
【问题描述】:

我正在学习 Haskell,并编写一个简短的解析脚本作为练习。我的大部分脚本由纯函数组成,但我有两个嵌套的 IO 组件:

  1. 从路径读取文件列表。
  2. 读取每个文件的内容,而这些内容又将成为程序其余部分的输入。

我有什么工作,但是嵌套 IO 和 fmap 层“感觉”很笨重,就像我应该避免嵌套 IO(不知何故),或者更巧妙地使用 do 表示法来避免所有 fmap。我想知道我是否过于复杂,做错了等等。这里有一些相关的代码:

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

我最终通过使用 listDirectory 作为 readFile 的输入来处理 IO [IO String]。并非无法管理,但如果我使用 do 表示法解开 [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 嵌套会变得多么复杂。

建议?

提前致谢。

【问题讨论】:

  • 嗨 - 欢迎来到 stackoverflow 和 haskell!恭喜你,我认为你是发现 (&gt;&gt;=) 有用性和 monad 概念的最佳方式,请参阅 this blog post
  • 还有sequence 或traverse = mapM 可能有用
  • join . fmap sequence :: IO [IO a] -&gt; IO [a] 很短。但似乎你只是想要,例如mapM getConfigs,首先。

标签: haskell


【解决方案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)

【讨论】:

  • 谢谢。不能投票给你,因为我对 SO 太陌生了,但这正是我所需要的。我也读过关于 mapM 的文章,但并没有完全理解它。对于其他偶然发现此内容的人(需要自己重读)...Monad tutorial in pictures
【解决方案2】:

您对“嵌套”的想法实际上是对 monad 是什么的一个很好的洞察。 Monad 可以看作是具有两个附加操作的 Functor,返回类型为 a -&gt; m a 并使用类型 m (m a) -&gt; m a 连接。然后我们可以使a -&gt; m b 类型的函数可组合:

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

所以我们想在这里使用 join 但现在有 m [m a] 所以我们的 monad 组合器不会直接提供帮助。让我们使用hoogle 搜索m [m a] -&gt; m (m [a]),我们的第一个结果看起来很有希望。是sequence:: [m a] -&gt; m [a]。
如果我们查看相关函数,我们还会发现 traverse :: (a -&gt; IO b) -&gt; [a] -&gt; IO [b] 与 sequence (fmap f v) 类似。

有了这些知识,我们就可以写了:

readConfigFiles path = traverse getConfig =<< getPaths path

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-09-17
    • 1970-01-01
    • 1970-01-01
    • 2013-06-15
    • 1970-01-01
    • 2017-01-31
    • 2013-05-01
    • 1970-01-01
    相关资源
    最近更新 更多