Deprecate like you mean it
entropicthoughts.com70 points by todsacerdoti 4 months ago
70 points by todsacerdoti 4 months ago
Please do the opposite. Let all deprecation warnings last at least a decade, just include in the warning that it is not maintained.
But more to the point, go out of your way to avoid breaking backwards compatibility. If it's possible to achieve the same functionality a different way, just modify the deprecated function to use the new function in the background.
My biggest problem with the whole static typing trend is that it makes developers feel empowered to break backwards compatibility when it would be trivial to keep things working.
edit: Not that it is always trivial to avoid breaking backwards compatibility, but there are so many times that it would be.
> just include in the warning that it is not maintained.
I'm convinced this isn't possible in practice. It doesn't matter how often you declare that something isn't maintained, the second it causes an issue with a [bigger|more important|business critical] team it suddenly needs become maintained again.
And here's where your business can contact me to talk about a support contract.
If it's important, they'll pay. Often you find out it wasn't that important, and they're happy to figure it out.
It sounds like you're imagining open source whereas the comment you're replying to is imagining more intra-company dependencies.
I think deprecation in intra-company code is a completely different beast. You either have a business case for the code or not. And if something is deprecated and a downstream project needs it, it should probably have the budget to support it (or code around the deprecation).
In many ways, the decision is easier because it should be based on a business use case or budget reason.
The business case is the easy part, the quagmire is in getting the different teams to agree who should support the business case, why it's more important than the business cases they wanted to spend cycles on instead, and how much of the pie supporting it takes on the budget side. Less so when the place is small enough everyone knows everyone's name, more so when it's large enough they really don't care what your business case is much even though it'd be 10x easier to support from their side instead of another.
Oh. But that is a solved problem. The users of the library just copy the code from before the deprecation and then stick it in their codebase not to be maintained anymore. Problem solved. /s
> I'm convinced this isn't possible in practice.
I don't agree. Some programming languages started supporting a deprecated/obsolete tagging mechanism that is designed to trigger warnings in downstream dependencies featuring a custom message. These are one-liners that change nothing in the code. Anyone who cares about deprecating something has the low-level mechanisms to do so.
For sure, but for this to work you need someone downstream to notice those messages and prioritize the work to migrate off the deprecated code paths. Some teams will respond, but many won't. No matter how loudly you declare that the code is deprecated, you'll still have people using it up to the point it stops working.
It's far better to plan the removal of the code (and the inevitable breaking of downstream users systems) on your own schedule than to let entropy surprise you at some random point in the future.
> For sure, but for this to work you need someone downstream to notice those messages and prioritize the work to migrate off the deprecated code paths.
Deprecation messages show up as compiler warnings. As a package maintainer, your job does not include taking over project management work in projects that depend on your package.
I don't know that I see why/how this is a problem? You would do the same with any other thing in your life?
More, in many things, we have actively decided not to do something anymore, and also highly suggest people not mess with older things that did use it. See asbestos. Removing it from a building is not cheap and can be very dangerous.
It also keeps slowing down development as getting a green global compile will make you still update "deprecated" functions that face breaking API changes.
> My biggest problem with the whole static typing trend is that it makes developers feel empowered to break backwards compatibility when it would be trivial to keep things working.
I don't see the connection you're drawing here.
Disclaimer: I’m a strong advocate for static typing.
I absolutely see the connection. One of the advantages of static typing is that it makes a lot of refactoring trivial (or much more than it would be otherwise). One of the side effects of making anything more trivial is that people will be more inclined to do it, without thinking as much about the consequences. It shouldn’t be a surprise that, absent other safeguards to discourage it, people will translate trivial refactoring into unexpected breaking changes.
Moreover, they may do this consciously, on the basis that “it was trivial for me to refactor, it should be trivial to adapt downstream.” I’ll even admit to making exactly that judgment call, in exactly those terms. Granted I’m much less cavalier about it when the breaking changes affect people I don’t interface with on a regular basis. But I’m much less cavalier about that sort of impact across the board than I’ve observed in many of my peers.
Rather, static typing empowers backwards compatibility, right?
Because it lays out the contract you have to meet on the interface. No contract? No enforced compatibility.
It is a way to signal to other developers that your code changed, which is better than not saying anything.
But it seems to make library developers more comfortable with making breaking changes. It's like they're thinking 'well it's documented, they can just change their code when they update and get errors in their type checker/linter.' When I think they should be thinking, 'I wonder what I could do to make this update as silent and easy as possible.'
Of course, we all have different goals, and I'm grateful to have access to so many quality libraries for free. It's just annoying to have to spend time making changes to accommodate the aesthetic value of someone else's code.
But it also makes it easier for library developers to identify that a change is breaking. Without static types you can easily make a change you think is fine, but it ends up breaking user code because they depended on something you didn't expect. Granted, static typing doesn't completely solve that problem, there can still be changes in behavior that break things, but it is a useful tool in identifying certain classes of breaking changes.
I think the only time I’ve ever seen this is in a JS library, where each major version bump is practically a different library altogether because it’s a complete rewrite, and these days is a wrapper around some Rust thing.
Not even JS alone. I blame the enforcement of semantic versioning, as if a version of code simply had to be a sequence of meaningful numbers.
If you are using a language with a complete type system, sure. But who uses those?
When using the languages people actually use in the real world, not really. Consider a simple example where the contract is that you return an integer value from 1 to 10. In most languages people actually use, you're going to be limited to using an integer type that is only constrained by how many bits it is defined to hold, which can be exploited later to return 11, unbeknownst to the caller's expectations. There are a small number of actually-used languages that do support constraining numeric types to a limited set of values, but even they fall apart as soon as you need something slightly more complex.
This is what tests are for. They lay out the contract with validation of it being met, while also handily providing examples for the user of your API to best understand how it is intended to be used.
More than anything, this reveals your lack of understanding of modern languages and type systems.
TypeScript, for example, is one of the most widely used languages in the world. It has an incredibly powerful type system which you can use to model a lot of your invariants. By leaning on patterns such as correct-by-construction and branding, you can carry around type-level evidence that e.g. a number is within a certain range, or that the string you are carrying around is in fact a `UserId` and not just any other random string.
Can you intentionally break these guarantees if you go out of your way? Of course. But that's irrelevant, in the same way it is irrelevant that `any` can be used to break the type system guarantees. In practice, types are validated at the boundaries and everything inside can lean on those guarantees. The fact that someone can reach in and destroy those guarantees intentionally doesn't matter in practice.
Typescript is one of the actually-used languages that supports constraining the integer, albeit in a pretty awkward way.
type Decade = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
But now try defining a type that enforces a RFC-compliant email address...There are languages with proper type systems that can define full contracts, but Typescript is not among them. Without that, you haven't really defined a usable contract as it pertains to the discussion here. You have to rely on testing to define the contract (e.g. assert the result of the 'generate email' function is RFC-complaint).
And Typescript most definitely does. Testing is central to a Typescript application. It may have one of the most advanced type systems found in languages actually used, but that type system is still much too incomplete to serve as the contract. Hence why the ecosystem is full of testing frameworks to help with defining the contract in tests.
You clearly don't understand the patterns I described. Look up branding and try reading e.g. "parse, don't validate".
"Parse, don't validate", while familiar to all HN users (it feels like it gets posted here every week), is orthogonal and does not address what we are talking about. To extrapolate, let's stay with the email address type example.
In Typescript, you can define an EmailAddress type as:
type EmailAddress = string & { __brand: "EmailAddress" }
If you are feeling saucy, you can even define it as: type EmailAddress = `${string}@${string}` & { __brand: "EmailAddress" }
But nether of these prove to me, the user of your API, that an EmailAddress value is actually a RFC-compliant email address. Your "parse" function, if you want to think in those terms, is quite free to slip in noncompliant characters (even if only by accident) that I, the consumer, am not expecting. The only way for me to have confidence in your promise that EmailAddress is RFC-compliant is to lean on your tests written around EmailAddress production.That isn't true for languages with better type systems. In those you can define EmailAddress such that it is impossible for you to produce anything that isn't RFC-compliant. But Typescript does not fit into the category of those languages. It has to rely on testing to define the contract.
Let's imagine we are working with some hypothetical language in which what you describe is possible.
At some point, you will have to write a function where you validate/parse some arbitrary string, and it then returns some sort of `Email` type as a result. That function will probably return something like `Option<Email>` because you could feed it an invalid email.
The implementation for that function can also be wrong, in exactly the same way the implementation for the typescript equivalent could be wrong. You would have to test it just the same. The guarantees provided by the typescript function are exactly equivalent, except for the fact that you do technically have an escape hatch where you can "force" the creation of a branded `Email` without using the provided safe constructor, where the other language might completely prevent this - but I've already addressed this. In practice, it doesn't matter. You only make the safe constructor available to the user, so they would have to explicitly go out of their way to construct an invalid branded `Email`, and if they do, well, that's not really your problem.