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

可以在Haskell中用纯代码读取文件吗?,haskell,io,parsec,megaparsec,Haskell,Io,Parsec,Megaparsec,我正在为DSL编写一个编译器。将源文件读入字符串后,所有其余步骤(解析、类型检查和codegen)都是纯代码,将代码从一种表示转换为另一种表示。在源文件中存在依赖项之前,一切都很好(想想#在C中包含预处理器)。解析器需要读取依赖文件并递归地解析它们。这使它不再纯净。我必须将其从返回AST更改为IO AST。此外,所有后续步骤(类型检查和codegen)也必须返回IO类型,这需要进行重大更改。在这种情况下,处理读取依赖文件的好方法是什么 p、 我可以使用unsafePerformIO,但这似乎是一

我正在为DSL编写一个编译器。将源文件读入字符串后,所有其余步骤(解析、类型检查和codegen)都是纯代码,将代码从一种表示转换为另一种表示。在源文件中存在依赖项之前,一切都很好(想想
#在
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]