我已经处理设计问题已有很长时间了,其中循环依赖性是基本问题,而我在解决它时遇到了一些问题。 我来自C,那里的循环依赖都是可能的,而且很容易解决。
以下是项目中感兴趣的文件的非常简化的图像:
ast.ml (实际上没有界面,我不太想复制整个类型)
type loc = string * (int * int) * (int * int)
and id = string * loc
and decl =
| Decl_Func of decl_func
and decl_func = {
df_Name: id;
mutable df_SymTab: sym_tab option;
}
(* goes on for about 100 more types *)
symtab.mli
type t
type symbol =
| Sym_Func of Ast.decl_func
val lookup_by_id: Ast.id -> symbol
(将来有更多文件要添加)
在C语言中,我只是使符号表成为指针,然后向前声明它。 问题解决了。不幸的是,这在OCaml中是不可能的。
每个实现都很大。这意味着我绝对不想制作所有递归模块,因为那将意味着实现文件将是10kloc甚至更多,带有大量与实际无关的代码(超出大型递归类型)。
在保持某种模块化设计的同时,我该如何解决呢?
答案 0 :(得分:1)
您并不是第一个遇到此问题的人,根据工作流程,口味和需求,有许多不同的解决方案。
这里是思考的好方法。
离开时,我的意思是像loc
或id
这样的类型不依赖于任何其他类型。它们不需要在您的递归类型定义中,因此也不必。
此外,您可能会具有处理位置和标识符的特定功能,并且使这些功能接近类型定义是一种好习惯。因此,您可以使用适当的定义和基本功能创建 ast_loc.ml 和 ast_id.ml 文件。
这看起来似乎很少,但实际上可以减轻 ast.ml 的额外好处,从而使代码更清晰。
现在,我不不建议您广泛使用它,因为它具有更多的间接性,往往使代码更难以阅读。检查一下:
type 't v = Thing of 't
(* potentially in a different later file *)
type t = Stuff of t v
通过使用类型参数,可以延迟类型定义中递归的使用。请注意,我不建议您在整个AST中使用它,因为它会使您感到痛苦,但是如果您有一些中间节点的行为与其余节点完全无关,那么这可能会有所帮助。
例如,这些可以经常使用:
type 'a named = { id : id; v : 'a; }
type 'a located = { loc : loc; v: 'a; }
如果此方法有助于分解类型定义,则该方法特别有用。但是,正如我已经说过的:不要滥用它!这很容易做到,但很难维护。
截止到今天,OCaml编译器的Parsetree
文件有958行。那是应该的。这是一个复杂的树结构,应该是可见的。
请注意,该文件只是类型定义。后续文件包含用于操纵该定义的代码(通常不会在其模块外引入必要的新类型)。
在某种程度上,我有点反对我对loc
和id
提出的观点,认为您应该将类型定义和代码分开,但这是另一种情况:loc
和id
是可以独立操作的简单类型。 symbol
仅在您的AST定义内有意义。另外,没有什么可以阻止您创建一个 symbol.ml 文件,该文件在不包含类型定义的情况下操纵AST的这一部分(注释是您的朋友,Merlin是必须的)。
此外,除非您确实需要递归函子,否则我不建议您使用递归函子。