Extending a module type

I have a module type like this:

module type Store = sig
  type data

  val save :
    key:string -> data -> (unit, error) Lwt_result.t

  val read :
    key:string-> (data, error) Lwt_result.t
end

and I have both a db and in memory implementation:

module Make (D : SerializableData) : Store with type data := D.t = struct
 let my_data = ref (...)

  let save ~key ~data = ...
  let read ~key = ...
end

Now, for the tests, in db it’s easy to clear the data (just clean up the db), but for the in memory, I would like to add a clear method that clear my_data on the in memory implementation, but I have no idea how.
I tried moving let my_data = ref... outside of the functor but then the types don’t match anymore.

What are the best options here?

Perhaps add a clear function to the module type and implement it in the functor?

I would approach this by defining a special interface for testing, e.g.,

module type TestableStorage = sig 
   include Store
   val test_setup : unit -> unit
   val test_teardown : unit -> unit
end

and implement this interface for testable storages I will implement this interface and run tests on them.

1 Like

may be a bit of a tangent but what’s the rationale for not having the facility to shrink a module type? as in the hypothetical

module type B =
sig 
  include A hiding x (* where x is defined in A *)
end

Yes you can remove the type by substituting it by it’s representation:

module type A = sig type x end

module type B =
sig 
  include A with type x := unit
end

of course you have to respect the module signature and cannot substitute the type with an incompatible one.

See the manual for more explanation

A more natural approach to narrowing the module type, is

module type A = sig val x : unit end
module type B = sig include A val y : unit end
module B : B = struct .. end
module B_narrowed : A = B
1 Like

I meant shrinking the module-type. as in having a module with e.g. 20 val/type definitions and wanting to have another module with 19, excluding one by name, without having to spell out the other 19’s types in a new signature in order to ascribe the 20-vals module to it.
sorry for not being quite clear in my wording haha

Yeah but this is quite restrictive (you need to specify the resulting module type rather than generating it via a hiding operation)
I realize this might not be that practically useful in real world code – evident by how well big codebases gone by with good expressiveness and without needing it. I’m just curious about whether it was ever considered and subsequently rejected (from a design standpoint), or if it’s just not a thing.

Thanks, this is exactly what I need.

This is how my implementation looks like now:

module type Store = sig
  type data

  val save :
    key:string -> data -> (unit, error) Lwt_result.t

  val read :
    key:string-> (data, error) Lwt_result.t
end

module type TestStore = sig
  include Store

  val clear -> unit -> unit
end

module MakeTest (D: SerializableData) : TestStore with type data := D.t = struct
...
end

module Make (D: SerializableData) : Store with type data := D.t = MakeTest (D: SerializableData)
1 Like