module - 循环依赖和模块化设计
问题描述
我已经处理一个设计问题已经有一段时间了,其中循环依赖是基本问题,我在优雅地解决它时遇到了一些问题。我来自 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 甚至更多,并且有大量不相关的代码(除了大递归类型)。
我将如何解决这个问题,同时仍然保持某种模块化设计?
解决方案
您不是第一个遇到这个问题的人,根据工作流程、品味和需求,有许多不同的解决方案。
这是考虑它的好方法。
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. 在某些时候,你需要一个大的递归定义
截至今天,Parsetree
OCaml 编译器的文件有 958 行。这就是它应该有的。这是一个复杂的树结构,应该是可见的。
请注意,该文件只是一个类型定义。后续文件包含操作该定义的代码(并且通常不会在其模块之外引入必要的新类型)。
在某种程度上,我有点矛盾我的观点,loc
并id
认为您应该将类型定义和代码分开,但这是一种不同的情况:loc
并且id
是可以独立操作的简单类型。symbol
仅在您的 AST 定义中才有意义。此外,没有什么能阻止您创建一个symbol.ml文件,该文件在不包含类型定义的情况下操作 AST 的那一部分(评论是您的朋友,Merlin是必须的)。
此外,除非您真的需要递归函子,否则我不建议您这样做。
推荐阅读
- android - Xamarin.Android 设计器是阿拉伯语的 VS 错误
- php - 上传文件 PHP EC2 不工作没有错误
- ruby - 使用 Ruby gets.chomp 方法向用户询问多个问题
- type-theory - 分离函数的联合类型?
- google-sheets - 在 Google 表格中查找奇数计数的公式
- r - 检查字符是否在数据框中
- android - 将值从单个传递到可完成以产生 rxjava Android
- javascript - 如何使用 webpack 生成 d.ts 和 d.ts.map 文件?
- c++ - 生成从 1 到 1000 的一系列字符的程序,其中包括数字和字母
- cordova - 在离子项目中运行 NPM 安装时出错