循环依赖和模块化设计

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

的叶子

我指的是像 locid 这样不依赖于任何其他类型的类型。它们不需要在您的递归类型定义中,因此不应该。

此外,您可能会有特定的函数来处理位置和标识符,并且让这些函数接近类型定义是一种很好的做法。因此,您可以使用适当的定义和基本函数创建 ast_loc.mlast_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 行。这就是它应该有的。它是一个复杂的树结构,应该是可见的。

请注意,该文件只是一个类型定义。后续文件包含操作该定义的代码(并且通常不引入在其模块之外必需的新类型)。

在某种程度上,我与 locid 认为您应该将类​​型定义和代码分开的观点有点矛盾,但这是不同的情况:locid 是可以独立操作的简单类型。 symbol 仅在您的 AST 定义中有意义。此外,没有什么能阻止你创建一个 symbol.ml 文件来操作 AST 的那部分而不包含类型定义(评论是你的朋友, Merlin 是必须的).

此外,我不建议使用递归仿函数,除非您真的需要它们。