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