Ocaml 模块依赖循环

Ocaml 模块依赖循环,ocaml,reason,Ocaml,Reason,我有: 单元1: 提供类型Module1.type1、其构造函数以及一些接受和返回type1 单元2: 打开模块1 打开模块3 提供类型模块2。类型2,还具有接受type1和type3作为参数的函数 单元3: 打开模块1 打开模块2 提供类型模块3.type3,及其依赖于类型1的构造函数 提供接受和返回类型type1、type2和type3 问题 结果,编译器显然得到了依赖循环:src/Module3.cmj->src/Module2.cmj->src/Module3.cmj错误。在

我有:

单元1:

  • 提供类型
    Module1.type1
    、其构造函数以及一些接受和返回
    type1
单元2:

  • 打开模块1
  • 打开模块3
  • 提供类型
    模块2。类型2
    ,还具有接受
    type1
    type3
    作为参数的函数
单元3:

  • 打开模块1
  • 打开模块2
  • 提供类型
    模块3.type3
    ,及其依赖于
    类型1的构造函数
  • 提供接受和返回类型
    type1
    type2
    type3
问题

结果,编译器显然得到了
依赖循环:src/Module3.cmj->src/Module2.cmj->src/Module3.cmj
错误。在TypeScript/JS中,通过单独导入可以实现的一些事情,在合理的情况下是不可能实现的。如何避开这个问题


我真的不想改变我程序的架构,只是为了简化编译器/模块系统的缺点。

看起来
Module1
不依赖于其他两个模块。你可以保持原样。但是,由于其他两个是相互递归的,因此可以使用递归模块语法来表示。这确实要求您在定义点声明模块签名,因为Reason需要知道预期结果。例如:

/* Impl.re */
module rec Module2: {
  type type2;
  let make: (Module1.type1, Module3.type3) => type2;
} = {
  ... actual implementation of Module2 ...
} and Module3: {
  type type3;
  let make: Module1.type1 => type3;
  let foo: Module1.type1;
  let bar: Module2.type2 => type3;
} = {
  ... actual implementation of Module3 ...
};
这是您将使用的一般形状,您可以根据需要进行调整

如果您不希望用户必须执行
Impl.Module2….
才能访问递归模块,您甚至可以使用
include
将它们公开为文件模块:

/* Module2.re */
include Impl.Module2;
您甚至可以使用编译时警告对实现模块(
Impl.Module2
和3)进行注释,让用户知道不要使用这些模块:

/* Impl.re */
[@deprecated "Please use Module2 instead of Impl.Module2"]
module Module2: {...} = {...};

处理问题的最简单方法实际上是递归模块。我不建议您使用它们,因为递归模块会使代码更难阅读、编译,并且在最复杂的情况下会在运行时破坏代码。更不用说,如果你在你的模块定义中使用副作用(请不要)

我将使用OCaml语法,您应该能够轻松地转换为理性

如果你想继续这样做,这里有一个快速而肮脏的解决方案,使用递归模块和函子

又快又脏的解决方案 1) 创建一个模块myModTypes,该模块将指示模块2和模块3的预期类型。它应该是这样的:

module type Module2type = sig ... end
module type Module3type = sig ... end
是模块的预期签名(如果您已经编写了接口文件,只需在此处复制/粘贴它们,如果您不编写这些文件,它们很重要)

2) 将module2和module3放入需要另一个模块的函子中

例如,module2的代码现在应该是

module MakeModule2(Module3 : MyModTypes.Module3type) = struct
(* the code of module2 *)
end
模块3的代码将采用相同的方式,只需在添加的行中交换2和3

3) 使用该代码创建模块MakeModules2和3(翻译为Reason):

请注意,递归模块定义总是期望模块类型

4) 以后使用
Module2
Module3
时,现在应该
打开makemodules2和3
,然后才能使用它们

正确的解决方案 您必须更改程序的体系结构。稍微

正如OP所说,函数中没有依赖循环,这是一种解脱。只需将模块2和模块3拆分为两个新模块即可。一个具有仅依赖于模块1的函数及其自己的模块,一个具有“下一步”函数


这是一种更好的方法来说明您如何声明模块:它们应该与它们定义的类型一致。理想情况下,每个类型都有一个模块,每个类型之间的交互都有一个额外的模块。

我希望我的回答能有所帮助,但也要意识到默认情况下不递归的模块确实是一个非常重要的安全和模块设计功能;它有助于保持模块的小型化和集中化,更重要的是,模块之间相互解耦。但正如您所看到的,有一种方法可以实现您想要的,更明确、更安全。在模块2和模块3之间,您是只引用类型还是使用在另一个模块中定义的函数?@PatJ也使用在另一个模块中定义的函数。@dark_ruby Yawar的答案可以解决您的问题,但是如果你改变了你对程序架构的看法,你会变得更好,你真的不应该像对待Typescript/JS那样用OCaml/Reason来思考你的架构。@PatJ谢谢,如果你能写一个答案,概述我如何使用这些类型来重新排列类型和函数,那就太好了,因此没有相互依赖性注意递归模块通常不是个好主意,它们非常不稳定。@PatJ怎么会…?@Yawar所以
module2
3
的实现在物理上会在同一个文件中?这就是我想说的avoid@Yawar递归将被动态解决,它可能会失败,并给您留下
未定义的\u递归\u模块
异常,它比常规递归或常规模块定义慢,错误在此类功能上比在其他功能上发生得更频繁。但这并不都是坏事,flambda团队目前正在努力解决这一问题。如果您想定义相互递归的类型,递归模块是一个好主意,但我不建议您将它们用于相互递归的函数。@dark\u ruby是的,它们在物理上位于同一个文件
Impl.re
。但正如我所解释的,您还可以从顶级文件中公开它们。用户不需要知道或关心实现细节。
module rec Module2 : MyModTypes.Module2type = Module2.MakeModule2(Module3)
and Module3 : MyModTypes.Module3type = Module3.MakeModule3(Module2)