I have a library where functors have proliferated. It was quite painful to functorize a module deep in the stack, because of types not being equal anymore in many places.
Therefore I wonder if there is a better way to organize the code so that it would be less verbose and less painful to evolve.
To illustrate, let’s imagine we have module types A
, B
and C
, then AB
that depends on A
and B
, and ABC
that depends on AB
, A
, B
and C
.
What should AB
expose, and what should ABC
take as input ? (and expose itself, for functors further down the line) ?
One possibility is to always pass everything as arguments, with all necessary with type
constraints, and to expose as little as possible.
So you would have
module ABC (A : A) (B : B) (AB : AB with type a = A.t and type b = B.t) (C : C) = struct
...
end
But once you have many arguments with many types, this can become very verbose, and it’s difficult to get all the right equalities.
Another possibility would be for AB
to re-expose A
and B
as submodules and only access them through there, so that you don’t need to pass them as arguments. Then when necessary you would use with module A = A
which is less verbose if you have many types.
Something like:
module AB (A : A) (B : B) = struct
module A = A
module B = B
...
end
module ABC (AB : AB) = struct
module AB = AB
...
end
Then when you need A
, you do ABC.AB.A
…
But I have the notion that this is not a great idea, as it’s easy to end up with multiple A
s that are not equal.
Yet another possibility, if you don’t actually want to parameterize on functor A B -> AB
implementations,
is to only pass A
and B
, and derive AB
in the body of the functor.
module ABC (A : A) (B : B) = struct
module AB = AB (A) (B)
...
end
This looks like it should work well, but I have also ended up with type equality problems that I don’t really understand…
So, what’s the best approach ? What rules should one follow to make it all fit together nicely ? Is that even possible or is any refactoring necessarily painful ?
Thanks a lot,