首页 > 解决方案 > 循环依赖和模块化设计

问题描述

我已经处理一个设计问题已经有一段时间了,其中循环依赖是基本问题,我在优雅地解决它时遇到了一些问题。我来自 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 *)

符号表.mli

type t
type symbol =
  | Sym_Func of Ast.decl_func

val lookup_by_id: Ast.id -> symbol

(以后还有更多文件要添加)

在 C 语言中,我只需将符号表设为指针,然后前向声明它。问题解决了。不幸的是,这在 OCaml 中是不可能的。

每个实现都非常大。这意味着我绝对不想让所有东西都成为递归模块,因为这意味着实现文件将是 10kloc 甚至更多,并且有大量不相关的代码(除了大递归类型)。

我将如何解决这个问题,同时仍然保持某种模块化设计?

标签: moduleocamlcircular-dependencymodularitymodular-design

解决方案


您不是第一个遇到这个问题的人,根据工作流程、品味和需求,有许多不同的解决方案。

这是考虑它的好方法。

1. 分离 AST 的叶子

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

此外,您可能会有特定的函数来处理位置和标识符,并且让这些函数接近类型定义是一种很好的做法。因此,您可以使用适当的定义和基本功能创建一个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. 在某些时候,你需要一个大的递归定义

截至今天,ParsetreeOCaml 编译器的文件有 958 行。这就是它应该有的。这是一个复杂的树结构,应该是可见的。

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

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

此外,除非您真的需要递归函子,否则我不建议您这样做。


推荐阅读