Search code examples
ocamlreasonbucklescript

Record field notes can't be found


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.


Solution

  • 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.