Multiple `with type`s in first-class module signatures results in confusing error message

Not sure if I should make a Github issue about this, but today I ended up burning a decent amount of time as a result of a confusing error message. Here’s a simplified example[1], which uses multiple with type constraints to constrain a first-class module parameter:

module type Graph = sig

  type t
  type vertex

  val empty : t
  val vertex : vertex -> t
  val overlay : t -> t -> t
  val connect : t -> t -> t

end

module Data (V : sig type t end) = struct

  type t =
    | Empty
    | Vertex of V.t
    | Overlay of t * t
    | Connect of t * t

  type vertex = V.t

  let fold (type g) (module G : Graph with type t = g with type vertex = vertex) : t -> g =
    let rec fold = function
      | Empty -> G.empty
      | Vertex x -> G.vertex x
      | Overlay (x, y) -> G.overlay (fold x) (fold y)
      | Connect (x, y) -> G.connect (fold x) (fold y)
    in
    fold

end
File "scraps/misc_algebraic_graphs.ml", line 23, characters 32-79:
23 |   let fold (type g) (module G : Graph with type t = g with type vertex = vertex) : t -> g =
                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: Syntax error: invalid package type: only module type identifier and "with type" constraints are supported

I thought the error meant either that:

  • multiple with type constraints were not supported with first-class modules, or
  • that constraining it by a concrete type (i.e. vertex) ws not supported

I tried working around this limitation by making a new module type that constrained the vertex type separately, but it was not an ideal solution:

module Data (V : sig type t end) = struct

  type t =
    | Empty
    | Vertex of V.t
    | Overlay of t * t
    | Connect of t * t

  type vertex = V.t

  module type Graph' = Graph with type vertex = vertex

  let fold (type g) (module G : Graph' with type t = g) : t -> g =
    let rec fold = function
      | Empty -> G.empty
      | Vertex x -> G.vertex x
      | Overlay (x, y) -> G.overlay (fold x) (fold y)
      | Connect (x, y) -> G.connect (fold x) (fold y)
    in
    fold

end

This made me wonder why OCaml had such a limitation. I tried searching around the issue tracker and the forum but could not find any information… then several hours after giving up, I stumbled on this post, which used and type to add secondary type constraints. Indeed, gong back to the documentation it seems like the grammar of the module-type and package-type non-terminals are subtly different! Is there an important reason for this, or is it an oversight? I.e. why isn’t package-type defined as:

  package-type ::= modtype-path
-              ∣   modtype-path with package-constraint { and package-constraint }
+              ∣   package-type with package-constraint { and package-constraint }

I think this would be much less surprising, if there’s nothing preventing it. Failing that, it would be nice if the error message could prompt you to use and type instead of with type. Something like:

Error: Syntax error: invalid package type: secondary "with type" constraints should use the "and type" syntax

I think there are probably better ways of presenting such and error - see Rust’s diagnostics, for example, but that’s another issue.


  1. Based on Algebraic graphs with class (functional pearl) ↩︎

Could you open a ticket in the OCaml bugtracker? The proposed syntax error change sounds pretty easy to implement and well defined

Can do! Was a little intimidated by the issue-creation thing so wanted to be sure :sweat_smile:

Edit: Posted here

1 Like