a package C, which explicitly depends on package B, which in turns explicitly depends on package A (this means that C implicitly depends on A),
a breakage of package C due to a major change in A (the removal of an instance) that reflects as non-major change in B (because it doesn’t use that instance).
The conlusion,
As a pragmatic solution, for now the PVP doesn’t required a major version bump in this case and instead leaves it to package C to add a dependency on package A to handle this situation.
seem to be more than pragmatic to me: when developing C, why should I not explicitly state my dependency on A?
I couldn’t help but thinking that that’s exactly the approach C++ uses, and that is encoded in the Core Guidelines - SF.10: Avoid dependencies on implicitly #included names. And C++ is anything but a niche/new/in-progress language, with even 5 years more than Haskell in its backpack, so its guidelines should not be dismissed lightly, imho.
Is there a reason why we don’t have an Avoid dependencies on implicitly imported entities in Haskell?
By they way, is there any centralized Haskell Core Guidelines at the present time or in some roadmap?
I think PVP’s solution isn’t pragmatic in the first place. Imagine you are reviewing a PR on package C and are trying to ensure that C has direct dependencies on all of the packages that provide instances used by C.
I think it’s also unreasonable in the C++ case, but C++ is simpler, so let’s look at that first. For each identifier referenced in new code in the PR, you need to find its provenance, and then check that all of those headers are explicitly listed in the file. And you should also go through to see which identifiers are no longer referenced in the code and see if any of those headers can be removed. Not impossible, but far from pragmatic, IMO.
Then the Haskell case makes it worse, by removing the identifiers. You now have to find the types of all the new identifiers used in the code, mentally do type resolution to determine the set of instances, and then find those particular instances (where some of the instances might be more general than the types you resolved), and add “empty” imports for any modules containing those instances that aren’t already imported. And again, you should also go through and do the reverse to determine which ones should be removed based on no-longer-used instances.
The removal is actually even more difficult, because you actually would have to go through the entire module not just the identifiers you’ve removed references to, since just removing `import Only.For.The.Instances ()` might still compile, but you’re now using some of those instances via transitive import, creating the situation you’re trying to avoid.
I have also never seen this be a problem in practice. People have generally been loath to remove instances and we only recently gained the ability to deprecate specific instances, so there’s probably not that much actual breakage of this type.
I’m surprised to learn that in this scenario, Package B is automatically re-exporting the instances it has imported from Package A. That seems to be the real source of the problem—what it’s exporting has changed without any change to its source code, nor any syntactic indication it’s re-exporting anything to begin with. (Probably I’ve benefitted from the convenience of this without realizing it, but I’m still surprised.)
If this weren’t the case, then Package C would have always had to declare its actual dependency on Package A in order to use the instance, and the removal of the instance would correctly not affect Package B (which wasn’t/isn’t using it).
That’s actually a good thought! I don’t think its necessary that typeclass instances be available without explicit import, though it certainly has elements of convenience. A flag to toggle this behavior would be an interesting option for GHC, and it could in turn be used as a “linting” pass even if it isn’t typically enabled.