I have this code in one module file :
type list_command = {
search : string option;
date : string option;
mime : string option;
filename : string option;
tags : string list;
}
type add_command = {
tags : string option;
filename : string;
}
type get_command = {
id : int;
tags : string option;
date : string option;
output : string option;
consult : bool;
}
(* List command implementation *)
let run_list_command env (db : Sqlite3.db) (cmd : list_command) =
let tags = List.map (fun t -> String.trim t) cmd.tags in
let documents : Common.document list = (Common.search_documents env db cmd.search cmd.date cmd.mime cmd.filename tags) in
List.iter (fun doc ->
Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at
) documents
And in the module Common I have the type definition :
type document = {
id : int;
created_at : string;
filename : string;
mimetype : string;
binary : string;
}
But I got the error
File “cli.ml”, line 35, characters 45-53:
35 | Printf.printf “%d: %s (%s)\n” doc.id doc.filename doc.created_at
^^^^^^^^
Error: This expression has type get_command
There is no field filename within type get_command
The type system wants really hard to ‘doc’ to have type ‘get_command‘ (its the only one of the same module type to have a field ‘id’).
It is happy when I explicit the type ‘(fun (doc : Common.document) → …‘
It seems the type system is bubbling up the types ?
Here’s a complete example
module Common = struct
type document = {
id : int;
created_at : string;
filename : string;
mimetype : string;
binary : string;
}
end
module OneModule = struct
type list_command = {
search : string option;
date : string option;
mime : string option;
filename : string option;
tags : string list;
}
type add_command = {
tags : string option;
filename : string;
}
type get_command = {
id : int;
tags : string option;
date : string option;
output : string option;
consult : bool;
}
end
open Common
open OneModule
let f doc = Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at
This fails to build with this error:
37 | let f doc = Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at
^^^^^^^^
Error: This expression has type OneModule.get_command
There is no field filename within type OneModule.get_command
It can be fixed the same way:
let f (doc : document) =
Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at
And you can make the error immediate go away, while likely having the same problem later, if you swap the two openlines.
The id from the later open is shadowing the id from the former. All bindings are like this. let x = 2 in let x = 1 in x evaluates to 1 and not 2. There’s nothing special happening in the type system AFAIK, nor any bubbling up apart from what you’re asking for.
There are a bunch of alternatives to opening modules in this recent thread about selective imports, and depending on what you’re doing exactly there might be even cleaner alternatives:
module Common = struct
...
let pp_brief fmt doc =
Format.fprintf fmt "@[%d: %s (%s)@]@." doc.id doc.filename doc.created_at
end
let f doc = Format.printf "%a" Common.pp_brief doc
module Common = struct
type document = {
id : int;
created_at : string;
filename : string;
mimetype : string;
binary : string;
}
end
module OneModule = struct
type list_command = {
search : string option;
date : string option;
mime : string option;
filename : string option;
tags : string list;
}
type add_command = {
tags : string option;
filename : string;
}
type get_command = {
id : int;
tags : string option;
date : string option;
output : string option;
consult : bool;
}
end
let print_document (documents : Common.document list) = List.iter
(fun doc -> Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at)
documents
(* File "test.ml", line 35, characters 50-52: *)
(* 35 | (fun doc -> Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at) *)
(* ^^ *)
(* Error: Unbound record field "id" *)
(* Mark set *)
let print_document2 documents = List.iter
(fun doc -> Printf.printf "%d: %s (%s)\n" doc.Common.id doc.filename doc.created_at)
documents
let print_document3 documents = List.iter
(fun (doc : Common.document) -> Printf.printf "%d: %s (%s)\n" doc.id doc.filename doc.created_at)
documents
Note that I have removed the two open statements.
I don’t understand why in ‘print_document’ the right type of ‘doc’ can’t be inferred from the type definition of the function argument ‘documents’.
In ‘print_document2’ the type is explicitly defined from the bottom and in ‘print_document3’ it is defined at one level up. That why I have the understanding the type system is sort of bubbling up.
In this line, the typechecker must resolve locally the field name id.
Since we are inside the body of the a function which only just introduced the doc argument, there are no information on the type of doc. Consequently, we chose the last record type introduced in scope that has a field id: get_command.
As you may have felt, this result depends on the order in which we are propagating type information, contrarily to the core part of type inference.
This is why type-directed disambiguation of fields is still considered to be a pragmatic but imperfect part of the OCaml type system (in other words, it is not principal (and there are some ongoing research works to try to improve this)).
4 Likes