Reputation: 7866
I have:
Module 1:
Module1.type1
, its constructor, and some functions that accept and return type1
Module 2:
open Module1
open Module3
Module2.type2
, also has functions that accept type1
and type3
as paramsModule 3:
open Module1
open Module2
Module3.type3
, and its constructor that depends on type1
type1
, type2
and type3
question
as a result I obviously get dependency cycle: src/Module3.cmj -> src/Module2.cmj -> src/Module3.cmj
error by compiler. Something that is trivially achievable in TypeScript/JS with individual import, is not possible in Reason. How to get around this?
I don't really want to change the architecture of my program, just to facilitate shortcomings of compiler/module system.
Upvotes: 3
Views: 2842
Reputation: 6144
The simplest way to handle your problem is indeed recursive modules. I don't advise you to use them, as recursive modules can make your code harder to read, compile and can in the most complex cases break your code at run time. Not to mention if you use side-effects in your module definitions (please don't).
I will use the OCaml syntax, you should be able to easily translate to Reason.
If you want to go with that anyway, here is the quick and dirty solution, using recursive module and functors.
1) Create a module myModTypes that will indicate the expected types of module2 and module3. It should look like:
module type Module2type = sig ... end
module type Module3type = sig ... end
with ...
being the expected signatures of your modules (if you already have interface files written, just copy/paste them here, if you don't write those, they are important)
2) Put module2 and module3 within functors expecting the other module
For example, the code of module2 should now look like
module MakeModule2(Module3 : MyModTypes.Module3type) = struct
(* the code of module2 *)
end
The code of module3 will be in the same way, just swap 2 and 3 in the added lines.
3) Create a module makemodules2and3 with that code (translated to Reason):
module rec Module2 : MyModTypes.Module2type = Module2.MakeModule2(Module3)
and Module3 : MyModTypes.Module3type = Module3.MakeModule3(Module2)
Note that recursive module definitions always expect a module type.
4) Subsequent uses of Module2
and Module3
should now open Makemodules2and3
before being able to use them.
You have to change the architecture of your program. Slightly.
As the OP said, there are no cycle of dependency in the functions, and that's a relief. Just split module2 and module3 into two new modules each. One with the functions that only depend on module1 and their own module, one with the "next step" functions.
This is a better way to approach how you declare your modules: they should be one with the types they define. Ideally, you have a module for each type, plus one additional module for each interaction between types.
Upvotes: 5
Reputation: 11627
Looks like Module1
doesn't depend on the other two modules. You can keep it as is. But since the other two are mutually recursive, you can express that using the recursive module syntax. This does have a requirement though that you declare the module signatures at the point of definition, since Reason needs to know what to expect. For example:
/* 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 ...
};
This is the general shape you'd use, you can adapt it to your needs.
If you don't want your users to have to do Impl.Module2....
to access the recursive modules, you can even expose them as file modules using include
:
/* Module2.re */
include Impl.Module2;
And you can even annotate the implementation modules (Impl.Module2
and 3) with a compile-time warning to let users know not to use those ones:
/* Impl.re */
[@deprecated "Please use Module2 instead of Impl.Module2"]
module Module2: {...} = {...};
Upvotes: 4