How do you treat changes to default settings for your library's configuration? Are you strict and consider them part of your API or are you more lax and allow changes to the default settings within a major version? My concrete use case looks similar to this:
I have a parser that runs a bunch of validators over the input.[1] Users can configure which validators they want to apply. My library, when finished, will provide 10s of validators out of the box. The sensible default in this domain will be to apply all built-in validators (seen in the Parser::new function), making them opt-out, rather than opt-in. Adding a new validator is not a breaking change. However, adding it to the list of default validators enabled in Parser::new could change the behaviour of a parser created with Parser::new between minor versions.
My approach would be to slap a big fat warning on the docs of Parser::new that it might change behaviour within minor versions and that users that are more particular about what input they want to accept should use Parser::with_validator instead. Would you consider this good practice? Are there any better options?
Of interest might be that validators don't overlap. Added validators don't constrict the valid input space but enlarge it. The input stream contains an identifier, each validator applying only to input that have their corresponding identifier present. More validators means more supported identifiers means larger valid input space. ↩︎
From the perspective “is this semver-compliant”, almost anything goes if you document it clearly and it’s possible for dependents to avoid breakage under the kind of change you intend to make.
Pragmatically, I would recommend not having a changing set of default validators under the name new(). If it were named something like with_recommended_validators(), that would be better.
I think Cargo's semver guide is quite useful: SemVer Compatibility - The Cargo Book. It is only concerned with breaking the compiler though, leaving this sort of—for a lack of a better word—"library semver" (borrowed from the "library UB" as in "library UB vs language UB" expression) open to interpretation. I usually like to lean into being more strict with my compliance claims, in this case however I see a real downside of making my API opt-in and not opt-out. What validators to apply will be mostly an implementation detail my users shouldn't worry about if they don't want to.
That's a good recommendation I intend to follow. I already differentiate between "stable", "experimental" and "deprecated" validators.[1] Naming the corresponding functions Parser::with_[stable|experimental|deprecated]_validators() would fit nicely with my model. I'm not too sure about the word "stable" though. Sounds too much like it won't change between minor versions to me. OTOH, "recommended" does sound a bit like they were hand-picked and not the set of all well-maintained validators. I'll have to think about the naming a bit more, I think expressing intent clearly here is quite crucial for API quality. If you have any recommendations, I'd be glad to hear them.
With possibly more fine-grained lists in the future as well. ↩︎
IMHO anything that may change the behaviour of a program without user intervention should be take intro account for semver. I would say that the change you showed in the example Is a major bump.
I'd really like to avoid bumping the major version every time a new validator shows up, as I expect this to happen rather frequently. I can see the library bumping the major version twice or more a year, only because of some default list most users will probably agree with. I'm afraid this will add more churn and hurt user experience more than validators being added / deprecated (a process that is well-known in the domain), without users noticing. Providing (I) a way for users to opt-out of the undesired default, potentially breaking behaviour, (II) clear documentation about the fact that validators may change between minor version bumps and (III) a descriptive interface where opt-in and opt-out options have equal standing (as kpreid suggested) would, IMO, be a better fit ergonomically in this case.
Never had to make that decision before though, which is why I'm asking here for your opinion. Usually I'd also try to err on the side of caution, not breaking users in potentially unexpected ways.
That is indeed a good name, although implementing Default for Parser would defeat the purpose of making opting into the potentially changing set of validators a conscious and explicit decision. But I like Parser::with_default_validators. What I have come up with besides "default", after sleeping on it, was "active". I think "active" vs "deprecated", rather than "stable" vs "deprecated", would also convey well what the set of validators represents.
something else to consider would be to group Validators under a symbolic name or model use-case; something basically similar to how base64 groups their Engines by character-sets, padding, URL safety, etc... in addition to the basic bare-bones offering of a with_vlidators(...) method that would allow users to explicitely pick what they want.
in my experience, as a user, having a library change the functionality of a public method with_<insert-name-here>_validators() potentially w/ each major release is a footgun. it strongly tells me to NOT use that method at all and instead use the one w/ an explicit collection of Validators i know i need.
This has made me wonder if anyone has ever tried to formalize the concept of breaking changes into a programming language. The "make every item content-addressable by hashing its AST" approach from the Unison language looks quite elegant in that regard. I have never used a content-addressing package manager before (e.g. Nix), let alone Unison, so I can't say for certain that using them is nicer than lockfile-based resolvers like Cargo. I have to say, the topic of dependency resolution has quite the nerd-sniping potential. Lots of great reading material out there, like Andrew Nesbitt's (creator of the Homebrew package manager) blog: https://nesbitt.io/.
Good analogy. To me your use case seems like a random function changing to a different distribution during the life time of the library. It isn't "wrong" per-se if the exact distribution was never documented but it is not something the user expect.
I agree that if the problem is just what the user expects, then good documentation can solve the problem for the majority of your users.