可以在Haskell中用纯代码读取文件吗?
我正在为DSL编写一个编译器。将源文件读入字符串后,所有其余步骤(解析、类型检查和codegen)都是纯代码,将代码从一种表示转换为另一种表示。在源文件中存在依赖项之前,一切都很好(想想可以在Haskell中用纯代码读取文件吗?,haskell,io,parsec,megaparsec,Haskell,Io,Parsec,Megaparsec,我正在为DSL编写一个编译器。将源文件读入字符串后,所有其余步骤(解析、类型检查和codegen)都是纯代码,将代码从一种表示转换为另一种表示。在源文件中存在依赖项之前,一切都很好(想想#在C中包含预处理器)。解析器需要读取依赖文件并递归地解析它们。这使它不再纯净。我必须将其从返回AST更改为IO AST。此外,所有后续步骤(类型检查和codegen)也必须返回IO类型,这需要进行重大更改。在这种情况下,处理读取依赖文件的好方法是什么 p、 我可以使用unsafePerformIO,但这似乎是一
#在C
中包含预处理器)。解析器需要读取依赖文件并递归地解析它们。这使它不再纯净。我必须将其从返回AST
更改为IO AST
。此外,所有后续步骤(类型检查和codegen)也必须返回IO类型,这需要进行重大更改。在这种情况下,处理读取依赖文件的好方法是什么
p、 我可以使用unsafePerformIO
,但这似乎是一个黑客解决方案,可能会导致技术债务。一个好的解决方案是解析为包含依赖项信息的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
解析仍然是纯粹的,您只需要在它周围有一个IO
包装器来驱动要加载的文件。您甚至可以重用此处的逻辑来加载单个文件:
load :: FilePath -> IO WithoutIncludes
load path = flatten $ WithIncludes [Include path]
在此处添加逻辑以检查导入周期也是一个好主意,例如,通过向flant
添加累加器,该累加器包含规范化FilePath
的Set
,并在每个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]
你能从概念上把它分成两个阶段吗?第一阶段识别并扩展包含的内容,另一阶段解析生成的“完整”DSL代码。这就是C的工作原理;C处理器在C解析器接管之前扩展其所有指令。您可以将类型更改为m AST
,而不是IO AST
。这可以防止任何不相关的IO潜入,您可以传入一个readExternal::FilePath->IO字符串来获取prod的IO版本,或者readMocked::FilePath->Identity String
以获得用于测试或嵌入的纯版本。如果WithoutIncludes
不File Void
以匹配之前的定义,会不会?还是我们没有这么做?@HTNW:两者都是有效的!如果您想保留Include的结构,并且结果是文件[Content“before”,Include[“between”],Content“before”]
,那么[String]
是有意义的;如果您想将所有内容展平,以便不再有Include
构造函数,Void
是有意义的,结果是File[Content“before”、Content“between”、Content“after”]
。Void
的问题在于,您仍然必须匹配Include
构造函数,并使用russible
说服typechecker您已经处理了该案例。我认为如果您想完全禁用构造函数,那么GADTs或更一般地说,与使用Void
填充类型参数相比,对构造函数的约束更符合人体工程学。例如,data CT=C | I;数据CI(t::CT),其中{Content::String->CI t;Include::FilePath->CI I}
和CI'C
禁用Include,或data Phase=Parsed |展平;类型family HasInclude(p::Phase)::Bool,其中{HasInclude'Parsed='True;HasInclude'flatted='False};数据CI(p::Phase),其中{Content::String->cip;Include::(hasclude p~'True)=>FilePath->cip}
如何添加错误处理?例如,包含的文件不存在parse::String->orrotype WithIncludes
&flatte::WithIncludes->IO(orrotype WithoutIncludes)
@sinoTrinity:您可以像这样使用orrotype
,并对结果进行模式匹配。使用Control.Monad.Trans.Except
中的ExceptT
可能更干净:当您有IO(ea)
时,您可以将其包装在ExceptT
中以获得ExceptT e IO a
,它允许您继续使用常规功能,如遍历
,而不是手动匹配左
/右
。要引发错误,请使用throwE
或except
,例如except(解析文件)
;要运行IO
操作,请使用Control.Monad.Trans.Class
中的lift
,例如lift(doesFileExist路径)
。最后,您可以使用runExceptT
将其作为IO(ea)
运行。
data File i = File [ContentOrInclude i]
data ContentOrInclude i
= Content String
| Include i
type WithIncludes = File FilePath
type WithoutIncludes = File [String]