循环依赖和模块化设计
Cyclic dependencies and modular design
我处理一个设计问题已经有一段时间了,其中循环依赖是根本问题,我在优雅地解决它时遇到了一些问题。
我来自 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 中,我只需将符号 table 设为指针,然后向前声明它。
问题解决了。不幸的是,这在 OCaml 中是不可能的。
每个实现都非常庞大。这意味着我绝对不想让所有东西都成为递归模块,因为那意味着实现文件将是 10kloc 甚至更多,其中包含大量并不真正相关的代码(超出大递归类型)。
我该如何解决这个问题,同时仍然保持某种程度的模块化设计?
您不是第一个遇到这个问题的人,根据工作流程、品味和需求,有许多不同的解决方案。
这是一个很好的思考方式。
1。分离你的 AST
的叶子
我指的是像 loc
或 id
这样不依赖于任何其他类型的类型。它们不需要在您的递归类型定义中,因此不应该。
此外,您可能会有特定的函数来处理位置和标识符,并且让这些函数接近类型定义是一种很好的做法。因此,您可以使用适当的定义和基本函数创建 ast_loc.ml 和 ast_id.ml 文件。
这可能看起来微不足道,但它实际上有助于使您的代码更清晰,并带来额外的好处 ast.ml.
2。如果需要,参数化你的类型
现在,我不建议您广泛使用它,因为它往往会使代码更难阅读,因为它有更多的间接性。看看这个:
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; }
如果此方法有助于分解类型定义,则此方法特别有用。但是,正如我已经说过的:不要滥用它!做起来容易,维护起来难。
3。在某些时候,你需要一个大的递归定义
截至今天,OCaml 编译器的 Parsetree
文件有 958 行。这就是它应该有的。它是一个复杂的树结构,应该是可见的。
请注意,该文件只是一个类型定义。后续文件包含操作该定义的代码(并且通常不引入在其模块之外必需的新类型)。
在某种程度上,我与 loc
和 id
认为您应该将类型定义和代码分开的观点有点矛盾,但这是不同的情况:loc
和 id
是可以独立操作的简单类型。 symbol
仅在您的 AST 定义中有意义。此外,没有什么能阻止你创建一个 symbol.ml 文件来操作 AST 的那部分而不包含类型定义(评论是你的朋友, Merlin 是必须的).
此外,我不建议使用递归仿函数,除非您真的需要它们。
我处理一个设计问题已经有一段时间了,其中循环依赖是根本问题,我在优雅地解决它时遇到了一些问题。 我来自 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 中,我只需将符号 table 设为指针,然后向前声明它。 问题解决了。不幸的是,这在 OCaml 中是不可能的。
每个实现都非常庞大。这意味着我绝对不想让所有东西都成为递归模块,因为那意味着实现文件将是 10kloc 甚至更多,其中包含大量并不真正相关的代码(超出大递归类型)。
我该如何解决这个问题,同时仍然保持某种程度的模块化设计?
您不是第一个遇到这个问题的人,根据工作流程、品味和需求,有许多不同的解决方案。
这是一个很好的思考方式。
1。分离你的 AST
的叶子我指的是像 loc
或 id
这样不依赖于任何其他类型的类型。它们不需要在您的递归类型定义中,因此不应该。
此外,您可能会有特定的函数来处理位置和标识符,并且让这些函数接近类型定义是一种很好的做法。因此,您可以使用适当的定义和基本函数创建 ast_loc.ml 和 ast_id.ml 文件。
这可能看起来微不足道,但它实际上有助于使您的代码更清晰,并带来额外的好处 ast.ml.
2。如果需要,参数化你的类型
现在,我不建议您广泛使用它,因为它往往会使代码更难阅读,因为它有更多的间接性。看看这个:
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; }
如果此方法有助于分解类型定义,则此方法特别有用。但是,正如我已经说过的:不要滥用它!做起来容易,维护起来难。
3。在某些时候,你需要一个大的递归定义
截至今天,OCaml 编译器的 Parsetree
文件有 958 行。这就是它应该有的。它是一个复杂的树结构,应该是可见的。
请注意,该文件只是一个类型定义。后续文件包含操作该定义的代码(并且通常不引入在其模块之外必需的新类型)。
在某种程度上,我与 loc
和 id
认为您应该将类型定义和代码分开的观点有点矛盾,但这是不同的情况:loc
和 id
是可以独立操作的简单类型。 symbol
仅在您的 AST 定义中有意义。此外,没有什么能阻止你创建一个 symbol.ml 文件来操作 AST 的那部分而不包含类型定义(评论是你的朋友, Merlin 是必须的).
此外,我不建议使用递归仿函数,除非您真的需要它们。