Haskell map 不会遍历整个列表

Haskell map does not iterate over the whole list

我正在尝试学习 Haskell 的基础知识,同时为 Pandoc 开发过滤器以递归地包含其他降价文件。

根据脚本指南,我能够创建一个有点工作的过滤器。这将查找带有 include class 的代码块,并尝试包含引用文件的 AST。

```include
section-1.md
section-2.md
#pleasedontincludeme.md
```

整个过滤器和输入源可以在以下存储库中找到:steindani/pandoc-include(或见下文)

可以 运行 带有过滤器的 pandoc 并使用以下命令查看 markdown 格式的输出:pandoc -t json input.md | runhaskell IncludeFilter.hs | pandoc --from json --to markdown

我注意到 map 函数 (at line 38) — 尽管获取要包含的文件列表 — 仅调用第一个元素的函数。这不是唯一的奇怪行为。被包含文件也可以有一个被处理的包含块,并且被引用的文件被包含;但它不会更深入,最后一个文件的包含块将被忽略。

为什么map函数不遍历整个列表?为什么在2层之后就停止了?

请注意,我才刚刚开始学习Haskell,我确定我犯了错误,但我很乐意学习。

谢谢

完整源代码:

module Text.Pandoc.Include where

import Control.Monad
import Data.List.Split

import Text.Pandoc.JSON
import Text.Pandoc
import Text.Pandoc.Error

stripPandoc :: Either PandocError Pandoc -> [Block]
stripPandoc p =
  case p of
    Left _ -> [Null]
    Right (Pandoc _ blocks) -> blocks

ioReadMarkdown :: String -> IO(Either PandocError Pandoc)
ioReadMarkdown content = return (readMarkdown def content)

getContent :: String -> IO [Block]
getContent file = do
  c <- readFile file
  p <- ioReadMarkdown c
  return (stripPandoc p)

doInclude :: Block -> IO [Block]
doInclude cb@(CodeBlock (_, classes, _) list) =
  if "include" `elem` classes
    then do
      files <- return $ wordsBy (=='\n') list
      contents <- return $ map getContent files
      result <- return $ msum contents
      result
    else
        return [cb]
doInclude x = return [x]

main :: IO ()
main = toJSONFilter doInclude

我可以在您的 doInclude 函数中发现以下错误:

doInclude :: Block -> IO [Block]
doInclude cb@(CodeBlock (_, classes, _) list) =
  if "include" `elem` classes
    then do
      let files = wordsBy (=='\n') list
      let contents = map getContent files
      let result = msum contents            -- HERE
      result 
    else
        return [cb]
doInclude x = return [x]

由于整个函数的结果类型是IO [Block],我们可以逆向计算:

  1. result 具有类型 IO [Block]
  2. contents 类型为 [IO [Block]]
  3. msum 与类型 [IO [Block]] -> IO [Block]
  4. 一起使用

第三部分是问题所在——在您的程序中,有一个非标准的 MonadPlus 实例正在为 IO 加载,我敢打赌它在 [=22] 上的作用=] 是这样的:

  • 执行第一个动作
    • 如果成功,产生与那个相同的结果并丢弃列表的其余部分。 (这是您观察到的行为的原因。)
    • 如果失败并出现异常,请尝试列表的其余部分。

这不是标准 MonadPlus 实例,因此它来自您正在导入的库之一。不知道是哪个。

这里的一般建议是:

  1. 将您的程序拆分成更小的函数
  2. 为这些函数编写类型签名

因为这里的问题似乎是 msum 与您预期的类型不同。通常这会产生类型错误,但在这里你运气不好,它与某个库中的奇怪类型 class 实例交互。


根据评论,您 msum contents 的意图是创建一个 IO 操作,该操作按顺序执行所有子操作,并将它们的结果收集为列表。嗯,the MonadPlus class isn't normally defined for IO, and when it is it does something else。所以这里使用的正确函数是 sequence:

-- Simplified version, the real one is more general:
sequence :: Monad m => [m a] -> m [a]
sequence [] = return []
sequence (ma:mas) = do
  a <- ma
  as <- mas
  return (a:as)

这让你从 [IO [Block]]IO [[Block]]。要消除双重嵌套列表,您只需使用 fmapIO.

中应用 concat