I'm trying define a variable with mutually recursive module, let's say a Todo can have many Note and a Note can belongs to a Todo:
module Sig = {
module type NoteSig = {type t;};
module type TodoSig = {type t;};
};
/* same file */
module Same = {
module rec Note: Sig.NoteSig = {
type t = {todo: Todo.t};
}
and Todo: Sig.TodoSig = {
type t = {
text: string,
notes: array(Note.t),
};
};
};
/* different files */
module A = {
module Note = (T: Sig.TodoSig) => {
type t = {todo: T.t};
};
};
module B = {
module Todo = (N: Sig.NoteSig) => {
type t = {notes: array(N.t)};
};
};
module C = {
module rec NoteImpl: Sig.NoteSig = A.Note(TodoImpl)
and TodoImpl: Sig.TodoSig = B.Todo(NoteImpl);
};
/* impl */
let todo1: Same.Todo.t = {notes: [||]};
let todo2: C.TodoImpl.t = {notes: [||]};
let todo3 = Same.Todo.{notes: [||]};
let todo4 = C.TodoImpl.{notes: [||]};
Js.log2(todo1, todo2);
However I cannot define a variable with this type, the compiler tells that:
36 │
37 │ /* impl */
38 │ let todo1: Same.Todo.t = {notes: [||]};
39 │ let todo2: C.TodoImpl.t = {notes: [||]};
40 │
The record field notes can't be found.
If it's defined in another module or file, bring it into scope by:
- Annotating it with said module name: let baby = {MyModule.age: 3}
- Or specifying its type: let baby: MyModule.person = {age: 3}
The same code in Ocaml if it helps:
module Sig =
struct
module type NoteSig = sig type t end
module type TodoSig = sig type t end
end
module Same =
struct
module rec Note:Sig.NoteSig = struct type t = {
todo: Todo.t;} end
and Todo:Sig.TodoSig =
struct type t = {
text: string;
notes: Note.t array;} end
end
module A =
struct module Note(T:Sig.TodoSig) = struct type t = {
todo: T.t;} end end
module B =
struct
module Todo(N:Sig.NoteSig) = struct type t = {
notes: N.t array;} end
end
module C =
struct
module rec NoteImpl:Sig.NoteSig = A.Note(TodoImpl)
and TodoImpl:Sig.TodoSig = B.Todo(NoteImpl)
end
let todo1: Same.Todo.t = { notes = [||] }
let todo2: C.TodoImpl.t = { notes = [||] }
let todo3 = let open Same.Todo in { notes = [||] }
let todo4 = let open C.TodoImpl in { notes = [||] }
let _ = Js.log2 todo1 todo2
Sorry for the long code, please discard these lines below.
First, the easiest solution is by far to make the types mutually recursive
type todo = { text:string, type todos:array(todo) }
and note = { todo:todo }
If you really need to split the two types in separate modules, them recursive modules are indeed necessary.
In this case, the key idea is that signatures represent a full specification of a module contents, In other words a signature
module type T = { type t }
is a specification for a module that implements a black box type t
and nothing else.
Consequently, the signature constraints Note:Sig.NoteSig
and Todo:TodoSig
in
module rec Note:Sig.NoteSig = { type t = { todo: Todo.t} }
and Todo:Sig.TodoSig = {
type t = { text: string, notes: array(Note.t)}
}
are actually erasing all information about the actual implementation of Note.t and Todo.t.
What do you want is to write the full signature first:
module type NoteSig = { type todo; type t = {todo: todo} }
module type TodoSig = {
type note;
type t = { text: string, notes: array(note)}
}
then you can write the implementation as
module rec Note: NoteSig with type todo := Todo.t = { type t = { todo: Todo.t} }
and Todo: TodoSig with type note := Note.t =
{ type t = { text: string, notes: array(Note.t)} }
If you only have types in your module, you could use the following version
module rec Note: NoteSig with type todo := Todo.t = Note
and Todo: TodoSig with type note := Note.t = Todo
For the functor version, if you don't need the functions defined in the module the easiest implementation is simply
module Make_Todo(Note: { type t;}) = {
type t = { text:string, notes:array(Note.t) }
}
module Make_Note(Todo: { type t;}) = { type t = { todo:Todo.t} }
(as a general rule, for beginners, it is generally better to let the typechecker infers the result type of functors.) then you can instance them with
module rec Todo: TodoSig with type note := Note.t = Make_Todo(Note)
and Note : NoteSig with type todo := Todo.t = Make_Note(Todo)
If you need more than the type of the other modules inside the make functor, you can go one step further by specifying that the argument of the functors implements the full signature
module Make_Todo(Note: NoteSig) = {
type t = { text:string, notes:array(Note.t) }
}
module Make_Note(Todo: TodoSig) = { type t = { todo:Todo.t} }
but then the instanciation of the module becomes sligthly more complex
module rec Todo: TodoSig with type note := Note.t =
Make_Todo({ include(Note); type todo = Todo.t })
and Note : NoteSig with type todo := Todo.t =
Make_Note({ include(Todo); type note = Note.t })
and there is more risks to encounter complex errors.