一个好的解决方案是解析成包含依赖信息的 AST,然后在解析器之外单独解析依赖关系。例如,假设您的格式可能是#include
一行或内容行:
data WithIncludes = WithIncludes [ContentOrInclude]
data ContentOrInclude
= Content String
| Include FilePath
和一个解析器parse :: String -> WithIncludes
,以便这些文件:
解析这些表示:
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
的 s,并检查每个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]