Open brprice opened 2 years ago
We observe similar issue with monomorphic types having identical names, but defined in different packages. I can assume that the same may happen inside single package if two modules define types with identical names.
IIUC all these problems arise from that only type names are used for constructing the final name.
An obvious solution to cover all these cases is to use package_name + module_name + type_name_itself
for final type name construction. Generic representation has access to all these bits, so technically this is easily doable. In that case T A
and T_A
would end up as pkg_T_pkg_A
and pkg_T_A
which are distinct (omitted modules for shortness). Similarly, in our case that would be pkg1_Foo
and pkg2_Foo
.
The downside of this approach is that names become too long and ugly(?) and in 95% of cases we don't need this redundancy. As a solution to this one could use (package, module, type_name)
(I believe something like that, i.e. unique type identifier, exists in TH, Cabal or other core packages, which we can re-use or use as implementation reference) as a key internally and smarter rendering of that tree, which would choose minimal combination of the 3 pieces above guaranteeing uniqueness. E.g. type_name
if there are no collisions (what we have now), pkg_name+type_name
(if 2 packages provide the same type) etc. Basically, the shorter one which is enough to uniquely identify the type across the whole API.
The question is how significant are required changes and how this will impact performance, maintainability etc
The downside of this approach is that names become too long and ugly(?) and in 95% of cases we don't need this redundancy.
Agree
As a solution to this one could use
(package, module, type_name)
(I believe something like that, i.e. unique type identifier, exists in TH, Cabal or other core packages, which we can re-use or use as implementation reference) as a key internally and smarter rendering of that tree, which would choose minimal combination of the 3 pieces above guaranteeing uniqueness.
However, I am worried about unintended consequences here. It seems like this approach would make it possible that adding a new type to your schema (/ adding a new route to your API) could change the names of pre-existing types. To me this would be very unexpected.
One (fairly manual) way around this would be to make the programmer choose how much disambiguation is required on a type-by-type basis (with some encoding such that a new definition can always be made distinct from any previous choices). Probably a deriving via
approach à la deriving-aeson could work for this.
It seems like this approach would make it possible that adding a new type to your schema (/ adding a new route to your API) could change the names of pre-existing types. To me this would be very unexpected.
Yeah, that's a downside. However, it is a relatively rare case which will occur only when changes introduce/remove name collisions and will affect only paths using relevant types.
Also the behavior could be controlled with a flag, so end-user could choose what is more preferable: API names stability or readability at cost of potential name shift. Conceptually,
data RenderNameOption
= AlwaysFullName
-- ^ Always use package+module+type
| Compact
-- ^ Like current behavior, but when required use package/module or their combination to prevent name clashes
One (fairly manual) way around this would be to make the programmer choose how much disambiguation is required on a type-by-type basis (with some encoding such that a new definition can always be made distinct from any previous choices). Probably a deriving via approach à la deriving-aeson could work for this.
Yeah, it's too "manual" but will work. Another drawback is that one needs tests to timely catch potential collisions.
As another option I can also imagine compile-time check for name conflicts (though not sure it is doable, especially, for T a
vs T_a
case), but another practical problem common for approach you've mentioned and compile-time check:
module Foo1
data Foo
2. Another library
``` haskell
module Foo2
data Foo
import Foo1 qualified
import Foo2 qualified
type API = Get Foo1.Foo :<|> Get Foo2.Foo
You can alter neither Foo1 nor Foo2, hence cannot affect name generation. The solution exists: newtype one of the conflicting names, but that's not ideal.
With schemas for polymorphic types, we can have names of schemas clashing, leading to incorrect generation (i.e. inconsistent with aeson's generic
ToJSON
). This can happen if one has a typedata T_A
and alsodata T a
&data A
: the generated schema names forT_A
andT A
are identical!As a complete example, consider the program
which has the output
Notice that the two
T_B
schemas have overwritten each other -- a serialised value of typeA
will not match this schema.Since types cannot contain the character
-
, one possible solution would be to change the serialisation introduced in #19 to useunspace ' ' = '-'
, rather thanunspace ' ' = '_'
.