可以在 Haskell 中以纯代码读取文件吗?

Possible to read files in pure code in Haskell?

我正在为 DSL 编写编译器。将源文件读入字符串后,其余所有步骤(解析、类型检查和代码生成)都是纯代码,将代码从一种表示形式转换为另一种表示形式。一切都很好,直到源文件中存在依赖项(想想 C 中的 #include 预处理器)。解析器需要读取依赖文件并递归解析它们。这使它不再纯粹。我必须将它从 returning AST 更改为 IO AST。此外,所有后续步骤(类型检查和代码生成)也必须 return IO 类型,这需要进行重大更改。在这种情况下,处理读取依赖文件的好方法是什么?

p.s。我可以使用 unsafePerformIO,但这似乎是一个可能导致技术债务的 hacky 解决方案。

一个好的解决方案是解析成包含依赖信息的AST,然后在解析器之外单独解析依赖。例如,假设您的格式可能是 #include 行或内容行:

data WithIncludes = WithIncludes [ContentOrInclude]

data ContentOrInclude
  = Content String
  | Include FilePath

和解析器 parse :: String -> WithIncludes 以便这些文件:

  • file1:

    before
    #include "file2"
    after
    
  • file2:

    between
    

解析为这些表示:

file1 = WithIncludes
  [ Content "before"
  , Include "file2"
  , Content "after"
  ]

file2 = WithIncludes
  [ Content "between"
  ]

您可以添加另一种类型,代表 扁平化 文件并解析导入:

data WithoutIncludes = WithoutIncludes [String]

并且与解析、加载和递归展平分开包括:

flatten :: WithIncludes -> IO WithoutIncludes
flatten (WithIncludes ls) = WithoutIncludes . concat <$> traverse flatten' ls
  where
    flatten' :: ContentOrInclude -> IO [String]
    flatten' (Content content) = pure [content]
    flatten' (Include path) = do
      contents <- readFile path
      let parsed = parse contents
      flatten parsed

则结果为:

flatten file1 == WithoutIncludes
  [ "before"
  , "between"
  , "after"
  ]

解析保持纯粹,您只需要一个 IO 包装器来驱动要加载的文件。您甚至可以重用此处的逻辑来加载单个文件:

load :: FilePath -> IO WithoutIncludes
load path = flatten $ WithIncludes [Include path]

在此处添加逻辑以检查导入周期也是一个好主意,例如通过向 flatten 添加一个累加器,其中包含 Set 个规范化的 FilePath,并检查在每个 Include 你还没有看到相同的 FilePath

对于更复杂的 AST,您可能希望在未解析类型和已解析类型之间共享大部分结构。在这种情况下,您可以通过是否已解析来参数化类型,并将未解析和已解析的类型作为具有不同参数的基础 AST 类型的别名,例如:

data File i = File [ContentOrInclude i]

data ContentOrInclude i
  = Content String
  | Include i

type WithIncludes = File FilePath
type WithoutIncludes = File [String]