Reexporting partially applied function in module signature

Hi,

I am running into a bit of a code duplication issue. I have a module with a signature (mostly to hide the implementation of type t) and I want one of the functions to return a curried function but I don’t want to copy-paste the signature. This is to 1) avoid duplication and 2) to allow the signature to change in the future without breaking my signature (changing it is fine).

Here’s an example:

module My : sig 
  type t
  val make : t -> [%please partially apply Uri.make]
end = struct
  type t = string
  let make t =
    Uri.make ~host:t
end

To make this type I would need to copy the entirety of Uri.make signature into my own signature, even despite me not caring at all what other arguments Uri.make takes — in fact I want the caller to get access to all the left-over arguments.

Is there any way I can do that in a straightforward way?

The only thing I can think of is to change my make function to take an additional argument f with (~host:string -> 'a -> 'b) but then the caller has to supply Uri.make by themselves which is… somewhat tedious.

How about something like this:


module Uri = struct
  let make ~(host: string) _other _args =
    `Complicated_return_type
end

module My = struct
  include (struct
    type t = string

    let to_string t = t
  end : sig
    type t

    val to_string: t -> string
  end)

  let make t =
    Uri.make ~host:(to_string t)
end

And here’s the externally visible signature:

# #show My;;
module My :
  sig
    type t
    val to_string : t -> string
    val make : t -> 'a -> 'b -> [> `Complicated_return_type ]
  end

In your shoes I would consider exposing a function to_host_string : t -> string. That way t is still abstract, but people can pass it to Uri.make or any other function. From an information hiding perspective, the implementation detail exposed by this function would already be effectively exposed by your make function anyway.

This is what I currently have essentially. The downside is that users have to remember to take host in this example, but in the real code it is host and port and scheme in all places and if you forget then it typechecks but does the wrong (default) thing.

@keleshev Yes, that works and is a creative way of selectively hide information (I’ll definitely steal this into my toolbox of tricks).

What I am less happy about is that I have to have an exported “accessor” function for each information that I need out of t that is not really supposed to be used outside of My. But maybe I can combine this approach with my idea of passing in the function and letting it be curried inside the inner module.

There is no straightforward way to do this, outside of copying the type of Uri.make. If you really really want to avoid such copy, the problem can be decomposed in two parts:

  • lift the type of an existing value into the module system
  • extract a subpart of a type

The first part can be done with first class modules (for types without any variables):

module type T = sig type t end
type 'a typed = (module T with type t = 'a)
let type_of (type a) (x:a): a typed = (module struct type t = a end)

The second part can be done with type constraints:

type 'a without_host =
  ?scheme:'b -> ?userinfo:'c -> 'd
 constraint 'a = ?scheme:'b -> ?userinfo:'c -> ?host:_ -> 'd

with the difficulty that labels must be explicits.
Finally, combining those two tricks yields

module Uri_type = (val type_of Uri.make)
module M : sig
...
  val make: Uri_type.t without_host
...
end
3 Likes