Hacker News new | past | comments | ask | show | jobs | submit login
Abstracting over ownership in Rust with higher-rank type bounds? (pocoo.org)
77 points by the_mitsuhiko on Sept 11, 2022 | hide | past | favorite | 67 comments



> do not abstact over borrows and ownership if functions are involved (unless you really know what you are doing).

I always think I know what I’m doing, then discover I in fact do not. My take away has been, don’t even do it even if you think you know what you’re doing.


You should update your priors then. Knowing when you're not sure what you're doing is a valuable metacognitive ability.


> As an example some of the intended changes to the compiler involving this kinds of stuff is on hold, because the change would break wasm-bindgen.

To not be able to fix the compiler because the fix would break a code gen tool used to interop with JavaScript seems like a priority inversion problem to me.


Rust has long had an aim as having the best out of the box tooling and support for WASM. wasm-bindgen is a significant part of that ecosystem. Given that WASM is a primary target for the language, ensuring wasm-bingen is working before a release seems perfectly reasonable.

I don’t think they would make the same decision for lesser used crates or tools that fail the crater build.

Seems like they have their priorities in good order.


It's also possible the GP interpreted this to mean "it won't ever be fixed in the compiler because that would be a breaking change for wasm-bindgen", which is a subtly different statement and I think I'd agree with them if that were the case


Rust promised to not introduce breaking changes willy nilly since 1.0.0. If the change would break some previously working code, they need to go through a deprecation period. This is probably the issue here.


As always when I see this claim, I feel compelled to point out that the Rust team’s claim is simply not true, since they don’t regard adding new functions to existing impls in the standard library as a breaking change, even though it can make working code stop compiling or even do something different.


And whenever this claim comes up, it's time to point to the API evolution RFC that outlines which kinds of breaking changes are acceptable or not: https://rust-lang.github.io/rfcs/1105-api-evolution.html

It's a classic communication problem. When we talk about breaking changes, we talk about what we have defined to be a breaking change. Obviously, if our definition didn't come close to approximating what a literal breaking change is in practice, then it wouldn't be a very good one. Similarly, if our definition were equivalent to a literal breaking change, then it also wouldn't be a very good one because it would prevent way too many types of additions that do not look like they should be a breaking change.

It's likely it would have been better to design the language such that there was no gap in the definition of "breaking change." But that never happened and probably never will.

As a mitigation mechanism to bridge the gap, we use a tool called crater that checks whether changes break compilation of published code. It is by no means perfect, but it helps. For example, if a change breaks code but would otherwise be acceptable API evolution, we will often not do it or otherwise proceed more carefully.


Ok, so why not say: “Our core maintainers internally use a definition of ‘breaking change’ that is different than the commonly accepted definition as a useful, and we feel necessary, fiction. This is detailed in RFC such-and-such, along with our rationale for this somewhat surprising choice. The reality is that we do break working production code past 1.0, though we invest great effort in trying not to, and we believe the benefits of being able to occasionally break production code outweigh the costs in practice”.

Linus doesn’t break userspace. Back when Raymond Chen’s clique at Microsoft ran the show, Windows would honor use-after-free in SimCity or whatever on a new kernel. They didn’t break working stuff. Python 3 took forever because they felt it necessary to tear out subtle but critical assumptions, e.g. around strings and UTF-8 and stuff. “We don’t break stuff.” is hard, it has serious costs to go along with it’s obvious benefits, and it’s not the only way.

It’s fine if Rust is going a different way, and it might be a revolution in this is a better way, we’ll know in twenty short years! :)

But trying to lawyer around it so that “Rust isn’t wrong, you’re wrong, just see Appendix F Subsection 42.6” when someone is staring at a broken build is not the Done Thing. It’s not particularly respectful to the people who bend over backwards to honor the common-law definition of “break”, and it’s a canonical example of what many hackers dislike about interactions with the Rust community.

I feel very strongly there is a more productive path here.


While maintainers may use a very technical sense of breaking change, that doesn’t mean in the end it doesn’t correspond to a lay usage of the term. The “ "Upgrading to a new stable compiler version requires either no changes or extremely small and easy changes to my code," question I posted below is what I would argue the lay understanding of the term means, and as the survey showed, it is extremely close to zero.

That you believe that the team goes “uhhh actually sorry your code isn’t broken due to subsection 2.4.f” shows that you don’t actually engage with Rust in this respect. Can you cite the Rust team breaking a bunch of user code and then responding this way when the inevitable outcry comes? I can’t think of a single time that that’s happened, though I am of course fallible. Given that you claim it happens regularly it should be easy! Literally the comment that started this sub thread is about the Rust team not making a change that would break a reasonably popular tool because it would harm users.


I don’t think there is any confusion around the lay definition. It’s roughly: “if it doesn’t build and behave the same on a stable point release, file a bug, it’s broken”.

It’s ok to break point releases, particularly when there is a clear policy on the upper bound of such breakage. I’m not critical of the policy per se, it’s a eminently plausible experiment in PLT versioning. It’s pros and cons are somewhat understood now, and in 10 or 20 years when there are billions upon countless billions of lines of Rust code in production (which there will be) it will be totally obvious how it panned out.

Just say “we break stable point releases in a subset of cases”. It’s ok. The thing that’s not ok, that I am critical of, is the seemingly boundless appetite of the Rust community to communicate in such a way that Rust is never wrong, or slower, or broken, or anything short of optimal. The optics are just wildly counter-productive here.

You can go to practically other language community and hear “yeah, that was a mistake in retrospect”, or “yeah, sorry about that, we’re working on it”, or “yeah, language Y is probably a better fit for your needs”.

All too often with Rust you get GP (who writes some of my favorite software and in general I deeply admire and respect) and some version of “no, Rust doesn’t break point releases, it’s you that doesn’t understand what ‘break’ means”.

This is not an isolated thing. It’s not the only example of that vibe. This weekend! It’s the default. And it’s not a good look.

Edit to reply to (unannotated) edit: You’ve got the wrong commenter with part of the new paragraph, I didn’t say it happens routinely or anything like that. You do have the guy who fixes his own Rust issues rather than going to the community for what I imagine are by now obvious reasons.


> All too often with Rust you get GP (who writes some of my favorite software and in general I deeply admire and respect) and some version of “no, Rust doesn’t break point releases, it’s you that doesn’t understand what ‘break’ means”.

That's not what I said. What I said was to clarify the situation and to note that actual practice matters.

I explicitly talked about the different meanings of "break" and acknowledged both of them as valid. And yet your portrayal of what I said makes it looks like I'm playing definitional games.

I fucking hate definitional games. I do not play them. I am not saying "that's not what it means" and then sticking my fingers in my ears and saying "la la la I can't hear you." What I'm saying is that there are multiple meanings to the word and talking about those meanings is a requirement for untangling the knot.

So when the libs-api team talks about "not breaking code," we do actually mean that in the literal sense. We care about not breaking code even if the change is compatible with our API evolution policy. But we still need a policy to outline what kinds of changes we can make because some of those changes can break code.

The intersperse method the others brought up is a perfect example of this. It's a change allowed by our API evolution, but we've specifically held back on it because it literally breaks code.


So I appreciate you replying and I hope you believe me that I am in fact a big fan. I use your software every day and have read a lot of it, so I know you’re a very serious hacker and I care about your opinions.

It’s not my intention to misquote anyone, especially someone who I respect a great deal, and I apologize to the extent that I have.

I think the disagreement is much more about tonality than the critical importance of the details, for context: I’m a former Rust hater who became a reluctant Rust advocate when I learned (a modern iteration of) it well and would like to be an enthusiastic Rust advocate. For every comment thread where I lose my temper with some of the more, zealous, Rust advocates you can find three where I’m advising someone to look at using it or arguing with the C++ or Haskell folks that they are being unfair to Rust.

My main obstacle to being an enthusiastic Rust advocate is (what is, in my opinion) a self-defeating extreme of enthusiasm and tribalism around Rust most concretely manifested in a “one language” philosophy.

Rust is very cool, and probably the best low-cost abstraction / performance-sensitive language with meaningful traction today.

But C++ has a place, and Python has a place, and Haskell, etc., etc. still have a place. One key example of where C++ and Python and Haskell have a place is that code still builds and still works after decades. This is a weak point of Rust: today a modest weakness, very recently a glaring weakness. So even if I misquoted you in particular: a mistrust around the semantic stability of Rust is a completely legitimate and merited concern, and it is not helped by saying anything other than “yeah, we’re less backwards compatible than alternatives, that makes sense because X”.

As someone who wants Rust to succeed (what 25-year C++ veteran wouldn’t): with the utmost respect, I strongly implore you to say that instead of bringing up an RFC.


OK, so I'll respond to what I see are the two "chunks" of your comment. The first chunk I see as "there are too many people zealously advocating for Rust." And the second chunk is, "say Rust has worse backward compatibility policy than C++, Python and Haskell instead of linking to an RFC." (I am "quoting" you here with the intent of phrasing my own internal understanding of what you're saying. The benefit of that is that if I misunderstood you, then perhaps you can correct that. It might make the conversation easier.)

So to the first chunk: absolutely. I don't really know what to do about it. You can find oodles and oodles of people with persistent misunderstandings about Rust and various aspects on both sides. Lots of people overstate what it can do, and lots of people understate what it can do. The overstatements tend to look like this:

* The Rust Project, as a whole, will literally never break your code.

* Rust's safety means that "once you compile it, it works." (A funny meme that I remember from my Haskell days. But now people also use it when talking about Rust.)

* Rust makes undefined behavior completely impossible.

Those are all overstatements. The most persistent understatement I see is, "well if Rust has 'unsafe' and everything boils down to using 'unsafe', then what's the point of Rust at all?"

I've spent time correcting all of these misunderstandings in various online forums.

I honestly do not know what the root of it is. I do not see people involved in the Rust project trying to push the overstatements. Most of the documentation materials I see maintained in the Rust project are sufficiently nuanced. It is just not obvious to me where "the Rust project is failing at communication" ends and "communication is heavily lossy and the general population is just going to fundamentally misunderstand most things because nuance has been stripped by the time the content filters down to them" begins.

I have my own mini-problems with this and ripgrep for example. Just the other day someone was trying to say that ripgrep was a "grep replacement" and so wasn't as suitable to code search as ack or ag. Like... wut? I don't see how it's possible to even skim ripgrep's README for less than 20 seconds and come away with that misunderstanding. Upon further prodding, I discovered:

* Their google-fu failed them.

* They "just assumed" from the name, because "grep" was in it.

* They were still a student and ultimately admitted they should have said they weren't "sure."

One of the most important lessons anyone can learn---especially an engineer---is to be clear about their certainty. I think it is just a fact of life that people aren't because they don't want to or maybe more likely, never learned the skill. (And it's even worse outside of engineering.) We could "fix" this by ensuring that everyone commenting online was always labeled with their experience level in a way that was true 100% of the time. Imagine if we could do that, hah. (And if we could, I'm not saying we should, but let's indulge.) I sometimes wonder just how strong the inverse correlation is between "says obviously wrong things with certainty" and "years of experience."

Anyway... sorry... Got off on a bit of a ramble there. Bottom line is that I agree it's a problem, but I don't know how it can be fixed. And there are other factors in play here, given that, ultimately, Rust really is trying to unseat some very old and very entrenched technologies. It is a complex sociological and cultural process.

OK, as to your second chunk... I want to quote the comment I was originally responding to:

> As always when I see this claim, I feel compelled to point out that the Rust team’s claim is simply not true, since they don’t regard adding new functions to existing impls in the standard library as a breaking change, even though it can make working code stop compiling or even do something different.

I am pretty sure that the ideal answer to this comment has to include some mention of our API evolution RFC. It is essential context for how we make decisions. What is less clear is that we don't really have (AFAIK) a solid written document describing how we deliberate on things that are permissible under API evolution, but break code in practice. There is a lot of grey area there. If the breakage is super rare, we might go fix that crate or issue a deprecation notice, wait some time and then make the change. That has happened. If the breakage is not so rare, then it's nearly guaranteed that we'll just drop it. Or find a way to smuggle it in over an edition boundary so that there is no breakage at all (you have to opt into it, and new Rust compilers still support the old behavior on the old edition).

I also note that in your second chunk, you go from talking about "in C++/Python/Haskell, decades old code still builds and works." But you then go on to talk about semantic stability in Rust. Why not compare apples with apples? Rust doesn't have decades of existence to leverage here.

To be quite honest, I would be very interested in analysis of actual decades old code of C++, Python and Haskell. For Python at least, we're talking about pre-2.7. And there were definitely breaking changes introduced in 2.7 when compared to 2.6. And that's just one release. For C++, they break stuff too! For example: https://stackoverflow.com/questions/6399615/what-breaking-ch...

I really truly believe that Rust is no worse than other programming languages when it comes to backcompat. More than that, I think it's better than most.


> Most of the documentation materials I see maintained in the Rust project are sufficiently nuanced.

I appreciate that you likely don't want to name and shame the documentation materials that aren't sufficiently nuanced, but do you think that any appreciable portion of the problem of over-zealous advocacy originates from those materials?


I honestly don't know. If there were examples I had at the top-of-mind, I would probably cite them.

remembers that the rust-lang.org web site changed quite a bit a few years ago and checks it again...

Yeah, there are some broad statements on the Rust home page that, if it were fully 100% up to me, would either not be there or be re-phrased. Unfortunately, commentary on the Rust homepage tends to get sucked into a black hole for reasons that I am not going to elaborate on. (And I'm not trying to say I 100% know all of the reasons. Some elude me.) It is definitely a problem. I don't know how to fix it. I've seen enough people try to suggest changes to the homepage and flounder that I just have zero interest in trying.

The other problem is that most of the re-phrasings I can come up with would use more words than what is there. It's hard to balance pithiness and precision.

Other than that one though, nothing is really coming to mind.

The other issue, perhaps, is testimonials. People can write their own lived experience, and sometimes it looks like a broader claim than it is. For example, I'd say this is generally true of my experience:

> Writing Rust is a very liberating experience, particularly compared to C, because I never have to worry about undefined behavior.

Here, "never" is hyperbole. It would be more precise to say, "99% of the time, I do not have to worry." For many people, it might actually be 99.9% of the time. It is empirically rare for me to need to write `unsafe` in application code, for example. As a similarish example, I use Go at work, and we just never ever use the `unsafe` package. That's not an apples-to-apples comparison, but it's not totally nutso either.

The issue here is that there are some people who use Rust who have to worry about UB quite a bit because that's the nature of the work they do in the context of Rust. So my experience is not universal. Even myself, I would guess that I use `unsafe` quite a bit more than the median programmer, because I tend to work on performance critical core libraries. But even then, it's a vast minority of the time.

So now let's say someone reads my experience and then goes and tells their friend:

> Hey have you seen Rust? It makes it completely impossible to have undefined behavior.

Now we've gone from lived experience to a claim about the language. Rust's goal isn't to make undefined behavior literally impossible. Rust's goal is to make it possible to build safe abstractions that are themselves impossible to use in a way that leads to undefined behavior. This is a deeply nuanced thing and it is super easy for it to get dropped from the context when discussing the language. Yet, the two claims are really quite different. And if the wrong claim is repeated in the wrong context, it really looks like over-promising and marketing-speak, which, rightfully, give people a bad taste in their mouths.

I'll again invoke ripgrep here, which has its own mini-problem with this too. The silver searcher does too. How many times do you hear people say things like, "grep took 2 hours to search my files, but ripgrep did it in 5 seconds!" Or "ripgrep is sooooo much faster than grep."

Like, there are sooooo many problems with saying that!

* Which grep? The performance profiles of GNU grep and BSD grep are radically different.

* Is this an apples-to-apples performance comparison?

* Is the comparison actually about the user experience? (ripgrep might skip that 10GB build artifact in your directory tree where as grep might not. So, if ripgrep isn't doing the same task, is it really fair to say it's faster? Well... yes, it is and no, maybe it isn't. Really depends on the context.)

* They might just have a measurement error. Maybe they tried grep first, which ended up measuring mostly disk I/O. Then tried ripgrep while everything was still warm in their file system cache. Oops. Benchmarking greps is hard.

What to do? I dunno. I try to fix things like this, but like, I can't respond to every tweet that says, "wow ripgrep was so much faster than grep."


> I've seen enough people try to suggest changes to the homepage and flounder that I just have zero interest in trying.

I'm guessing most of those people don't have the same level of respect within the Rust community that you do. I could be wrong, though.

> It's hard to balance pithiness and precision.

IMO Rust itself favors precision, so the website should as well.

On the one hand, I think Rust's momentum, including adoption by big industry players, is now strong enough that the website team could afford to avoid even the slightest appearance of over-aggressive marketing. There have been successful programming languages without marketing campaigns, after all; perhaps the best example is C (unless there's some aspect of its history I don't know). On the other hand, maybe discussions on message boards like this one make this problem look more important than it is, and the website is doing the right thing. I don't know.

Good points about testimonials being misunderstood and distorted.

Thanks for taking time to write a thoughtful response.


Paul Graham has thoughts on people who “market” programming languages: http://www.paulgraham.com/javacover.html

I’m the spirit of Postel’s Law I decided to actually learn Rust well before becoming truly dedicated to clowning on super fans.

I’m into basically anything if we go all out, as long as Rust is sponsored by a KPop group and a league team fuck hating: let’s make an NFT!


So first off, it's a pleasure to interact with a senior and mature member of the Rust team and community. I mean no disrespect when I observe that I too was once young and hyper-enthusiastic about this or that technology, and now being old I regrettably still go "Internet tough guy" more often than any adult should (cue obligatory XKCD), less with time thankfully: so I do in fact get it on some level where the more zealous Rust community members are coming from.

I can't add a lot to what is clearly a thoughtful and passionate analysis from someone who has spent serious time in the trenches of trying to keep both the haters and the zealots at least somewhat on the rails of reality and I applaud your efforts to do so.

Rust is an interesting phenomenon in many senses including this one: it gets hackles up on hackers like me who on average kind of already went through that phase. I think this is because I can already see that it's going to be a Really Big Deal™ for the rest of my entire career, and so at any present-day level of participation, before I hang up the keyboard the culture of Rust is going to be a big part of my world.

And I am far and away more concerned about the former "chunk" than the latter: any remaining loose ends (if there even are any) on Rust's backcompat story will no doubt be tidied up quite soon. I am not only willing but eager to concede the entire latter "chunk" because it's a distraction at best from the importance of the former one.

I'll sort of leave it here: I know that the core team is composed of serious, tenured, hardass hackers who know both C++ and Haskell and made all the decisions that C++/Haskell people criticize thoughtfully and deliberately, that is obvious from reading GitHub. With that said, technically amazing things (like Rust) routinely "fail to achieve their potential" because of culture/vibe/tone factors, and I regard this as the only serious risk to Rust's long-term success. You want the zillion people like me wholeheartedly rather than reluctantly advocating it.

My suggestion, however clumsily phrased initially, is to use your influence in the community to put downward pressure on the zealot stuff even if that requires you to err on the "under-promise and over-deliver" side of things in places like HN.

In the big picture view, this is detail stuff: you folks won, from here it's execution.

P.S. as an `rg` fanboi: I <3 @burntsushi


I do try to apply that downward pressure, but maybe not as much as I should. Point taken.


At the risk of beating a dead horse, I want to make a few suggestions in what I hope is a spirit of constructive feedback.

I think that the Rust community has a tendency to highlight the wrong selling points. Great selling points of Rust right off the top of my head:

* True innovation in memory management, others have done credible experiments with linear and/or affine typing, but Rust has made it work, at scale and in production, this is the first time anyone ever has, and it affords a very compelling compromise between more manual approaches and less performant approaches.

* "Just Works" package management and build: Cargo and the package ecosystem work by default, instead of "work via incredible effort" like C++ or (nowadays) Python. Rust insulates you from stupid platform idiosyncrasies in a way that seriously help you get shit done. There is a tradeoff here, you build a lot of stuff and so the advantage over C++ build times isn't what it could be, but it's basically impossible to overstate how much the rubber meets the road here on both getting started and shipping industrial strength stuff. It's massive.

* A vastly superior type system vs. C++ in basically all the hot paths. Rust has sensible, ergonomic, and pragmatic ways to express parameterized types, to pattern match on them rather than some 1970s `switch`-statement bullshit, to detect more unsafe casts, and to save you from quandaries (return value or out reference? ahh wtf do i do?) that some modern languages cough continue to punt on to the developer to this day. Interface vs. implementation sits in a very sweet spot between all the various directions that question gets tugged in. Bottom line: it's harder to write logic bugs in Rust. You can do it, but Rust helps you here a lot.

* Bundled static analyzer: yes, you can get much if not most of the static analysis that `rustc` gives you by cranking `clang-tidy` up all the way: but first you have to install it, and maintain that installation, and argue with your colleagues about which checks should be on, and, and, and. Rust mandates a sane, curated list of good static checks that come for free and by default. This is also hard to overstate in terms of the practical defect count in many shops.

A few bad selling points of Rust right off the top of my head:

* Memory Safety: Rust does in fact nudge you away from some memory safety issues and reduce the tooling effort involved in `clang-tidy` and `ASAN` effort to get memory safe programs, but it's just not that big of a deal outside of security critical stuff. C++ evolved a good story here while Rust was busy chest-thumping about memory safety, while malicious actors simultaneously evolved their techniques far past buffer overrun. Seriously, mention memory safety in passing or not at all: people like me want to throw something when they hear some JS -> Rust emigree mouth off about it.

* The "Right" Type System: People who know and care about exploiting types well (and understand they are going to pay in runtime performance) are now arguing about Haskell vs. Idris vs. PureScript -> WASM. Rust has a competent and pragmatic type system, but it's Haskell-lite at best in this regard. Compare Rust to C++ on typing, not serious math stuff.

Developer Efficiency in All Cases: Rust has serious developer velocity advantages over it's competitors, i.e. C++, not against all other languages. If you want something working ASAP, Python is better, if you are willing to pay runtime cost and learn shit to go faster over time, Haskell is better. Rust sits in an awesome sweet-spot of fast execution, way above average developer velocity, and quite good tooling. It's not faster than Python for "who cares" and it's not faster than Haskell for raw power. Being the best tradeoff in many cases is plenty, claiming to be the "best" full stop is ridiculous.

Rust's selling points are extremely compelling, and it has a very clean shot at being the "new C". This is plenty, don't overreach. It's a good thing that Rust will never be as expressive as Haskell, as good for numerics as Julia, as good for quick and dirty as Python, or as good for world-wide interop as C: being any of those things would make it worse at what it is!


> Rust community to communicate in such a way that Rust is never wrong

Just read about Async Rust to see how critical of Rust the Rust community can be!


Haha fair play, async does seem to have been a bit of a journey: but it also seems to be shaping up nicely, and Rust is in good company vis-a-vis the every other programming language that has struggled to find a good story here.

I’m case there is any confusion about whether or not I value Rust: literally my current work pertains to designing a DSL and compiler for async state machines (not totally unlike MSR’s P Language) because I need a way to compose extremely complicated async programs with hard correctness and performance properties.

And while the implementation language is Haskell and the first target language is C++ because this is for $DAYJOB and I need C++ first, I have also gone to great pains to avoid anything that will stop the next target language from being Rust: it will soon generate idiomatic Rust code that ties into Rust’s Async story because I really like Rust’s Async story and I think/hope that Rust will be my primary low-cost abstraction language ASAP.


Yeah and note that this RFC is older than 1.0, so very likely older than any of the code that was "broken" by the addition of a function.

Also note that you have to go out of your way to get broken by such additions: you have to declare a trait, then implement that trait for a group of types that you didn't define, and then use that trait on standard library types. Only then will you come into conflict. Also, if you used a name that would never end up in the standard library, you are also safe.

Last, often additions of functions are held back because of the breakage they would cause.


> Also note that you have to go out of your way to get broken by such additions: you have to declare a trait, then implement that trait for a group of types that you didn't define, and then use that trait on standard library types.

There's some mild irony here, in that you're replying to burntsushi, who just released 1.0 of a library that employs this pattern: https://news.ycombinator.com/item?id=32762345

I also think you might be downplaying the ease of hitting of the problem, too. While RFC1105 declares that this sort of breakage is acceptable, Rust's maintainers are, in practice, (understandably) hesitant to break builds. The stabilization of `Iterator::intersperse` was aborted after it was discovered that doing so would break the no-less-than 319 crates that directly or transitively depend on `Itertools::intersperse`: https://github.com/rust-lang/rust/pull/89638

Speaking as one of the maintainers of itertools: This whole situation feels kinda awful, and kinda makes me want to discourage others from using itertools. :(


> who just released 1.0 of a library that employs this pattern

oh interesting, the irony lol. Indeed there are many identifiers in there that cause the danger of breakage should the standard library ever add them.

> would break the no-less-than 319 crates

Wow that's a lot. I guess this problem scales with the size of Rust, which is ever increasing in number of users as well as crates. As an anecdote, one of my first additions to the standard library also ran into this, but at a way smaller scale: https://github.com/rust-lang/rust/issues/41793

Back then we didn't have the PR yet that would make nightly functions not conflict with downstream functions: only if you enabled the nightly function the conflict would exist. So it would move that breakage from the introduction to the function to its stabilization.

> and kinda makes me want to discourage others from using itertools. :(

What you get by this pattern is a shared namespace: shared by both itertools and std libraries. In C, every non-file-private function is part of the shared namespace, and there, libraries have evolved to prefix their function names by the name of the library. So if two libraries have a check_for_errors function, there is no conflict as they are named libfoo_check_for_errors and libbar_check_for_errors. So my suggestion would be to change itertools in that pattern. Or you might even use a shortening of the itertools name like itls, as long as nobody would ever add a function name like that to the standard library.


Yeah I feel like 0 breakage is just an unnecessarily strict goal. I like Typescript's approach. They just try to minimise breakage.

Also probably worth noting that JavaScript adds new functions that could break existing code all the time and I haven't heard anyone ever complain about it.


When people on HN claim that the Rust team does not break code that worked in previous language versions, they are not citing that RFC, they are using the term “breaking change” in its usual plain English meaning.

I think it is not just an issue of semantics: there is actually a widespread popular belief that the Rust project guarantees no breaking changes except to fix bugs, which is simply not true.

Perhaps the Rust team itself is careful not to actually claim this, in which I retract my assertion that they have done so. But it’s certainly something that is heavily implied as a selling point of the language.


Both things can be true. It can be true that we (the Rust team, I am on libs-api) are careful about what we claim while the "plain English" version of it is also true. I explained the mitigation measures we take, and it is quite good at telling us whether a change is going to break people, even if it is strictly acceptable from our API evolution policy.

I'm not trying to make a just a dumb semantic point here. We really do take the literal notion of breaking change seriously too.

The last time I myself experienced a breaking change was maybe 5 or 6 years ago. Neither ripgrep 0.2.0 nor 0.3.0 compile on current Rust. Although neither of the breaking changes came from library APIs.


I have seen many discussion through the years about how important semantic versioning is. Then every project seems to end up using their own definition of breaking change, which makes semantic versioning seem overhyped as a universal tool (although probably useful for each individual project because they know exactly what it entails).


Not sure what relevance that has here. Semver is just a tool to manage communication among a group of decentralized humans. Communication is always hard.

I personally use semver where I can, especially when I intend for others to use my code, because it is exceptionally good at signaling "this release may require some manual changes or extra work." And because it's good at that, tooling has been built up around it that makes it ever better.

Of course, semver has exceptionally small bandwidth. And sometimes people can disagree about what a breaking change is (does increasing the toolchain version required to build your library constitute a "breaking change?"). And sometimes behaviors of code are so difficult to specify that breaking changes may happen unintentionally.

And yes, things get overhyped. That's been happening with all sorts of things in tons of different contexts. I fail to see how it's an interesting remark in this case.


> Semver is just a tool to manage communication among a group of decentralized humans.

Some people have argued that the main point is about being machine-readable. But that’s not relevant as long as the consumers are all on the same page (users of NPM probably don’t have to care that much about what definition Rust uses).

> Of course, semver has exceptionally small bandwidth.

Yes. Just a flag in the breaking-change case. Maybe a data exchange format would have been better if we really wanted to communicate precisely what we have changed (again, if machine-readability was ever a concern).

> And sometimes people can disagree about what a breaking change is

The Semver 2.0 page defines it as a change to the public API (or “the API” since if the API is not public then it doesn’t even matter). So if you define your API then you should know what it would take to “break” things.

> I fail to see how it's an interesting remark in this case.

Sorry to have bothered you. Again.


And the rust team has defined their api, and some subset of users disagree.

You see this every time with semver. Is a bugfix that changes behavior someone relied on breaking? Is a performance drop? Is a performance improvement? All of those can break my code!


> every project seems to end up using their own definition of breaking change

This is natural. People naturally have different notions of what a breaking change is. For example, in C++ land, an incompatible change to an ABI is a breaking change, whereas in Rust land there's only a handful of types with stable ABIs. What "breaking" means will differ by language and ecosystem, and the goal is to find a useful definition for your particular context. Semver acknowledges this, which is why it deliberately refuses to strictly define what constitutes a breaking change.

Let's also keep in mind that every language makes tiny breaking changes whenever they think they can get away with it, including legendarily-stable enterprise languages like Java and PHP (look up the "compatibility guides" that accompany language releases). And that's not a bad thing; engineering is a science of tradeoffs.


As Ritch Hickey puts it semantic version is meaningless in practice.

https://youtu.be/oyLBGkS5ICk


You may want to re-read roblabla's post.

The Rust team has never promised that they will never break existing code, since that would lock them into a situation similar to C/C++. Making occasional rare changes that break working code (but for which it's possible to write code that works in both versions) is not "willy nilly".

For example, the following compiles with the current stable version (1.63.0):

  use std::ffi::*;
  
  mod my_ffi {
      pub type c_char = u8;
  }
  
  use my_ffi::*;

  fn main() {
    println!("c_char is {} bytes", std::mem::size_of::<c_char>());
  }
But it will break when 1.64.0 is released, because then `c_char` would become ambiguous.


C and C++ do have enough breaking changes.

Dropping K&R C syntax, exception specifications, auto_ptr,....


There is a spectrum from obvious breaking changes (removing a method), to non-obvious ones (trait methods, glob imports), and beyond (https://xkcd.com/1172/)

Before adding methods to a trait, they test the change against all public Rust code known to exist, to verify no breakage. Very occasionally this causes them to put plans on hold or work with downstream library authors (see: the num-traits crate)

For the benefit of closed-source code, they also emit a warning before the change is made, so you have time to prepare or give feedback (https://doc.rust-lang.org/stable/nightly-rustc/rustc_lint/bu...)

It's a perfectly reasonable policy. I'm not sure what more they could do, unless you expect them to literally never change any traits ever again and ossify the stdlib into whatever state it was in 2015 when Rust 1.0 was released. If that's what you want, pin your compiler/library versions and you can have it!


I’m fine with the current policy. What I would like them not to do is advertise “no breaking changes” as a major selling point of Rust, when that isn’t actually true.


Where do you see the Rust team claiming they will make no breaking changes?

Note: answers like "an anonymous nobody on Twitter says Rust is better than <other language> because it never breaks compatibility" get zero points.


Here is Steve Klabnik, who is on the rust team (or at least used to be), claiming that code that doesn’t compile on a newer language revision “relies on unsound things”: https://news.ycombinator.com/item?id=32063749


That is not what Steve is claiming.

The exact quote is:

  > > code written 5 years ago will often not compile today.
  >
  > citation needed. Yes, there's a few programs that relied on unsound
  > things for which this is true, but that's a relatively small part of
  > the overall amount of code.
In other words, there are a small number of packages that were doing things outside the bounds of what the Rust change policy guarantees -- for example unintentionally relying on type-checking bugs, or unstable features accidentally stabilized, or implementing traits on a struct when the library doesn't control both of them.

This does not mean that Rust code will "often" not compile. It is possible for Steve (and the public at large!) to know this, because the impact of breaking changes are measured by Crater.

The alternative choice, to prevent compiler/stdlib changes from being able to break any valid program, would halt language evolution. We've seen this in languages like C and C++, where the standards bodies make a truly heroic effort to avoid breaking code, but (1) they stop being able to evolve the language, and (2) code breaks anyway because there's always someone who's skirting the edge of permitted extension.


> for example unintentionally relying on type-checking bugs, or unstable features accidentally stabilized, or implementing traits on a struct when the library doesn't control both of them.

One of these things is not like the other. Implementing your own traits on foreign types isn’t “unsound”, it’s an absolutely normal and reasonable thing to do.

Steve specifically said “unsound”, which means something specific. He didn’t say “doing things outside the bounds of what the Rust change policy guarantees”.


  > One of these things is not like the other. Implementing your own traits
  > on foreign types isn’t “unsound”, it’s an absolutely normal and reasonable
  > thing to do.
It's possible to do, and there's good reasons to do it, but you have to be extremely careful about either (1) specifying all method calls unambiguously, or (2) extensive testing for dependency upgrades. It's very similar to wildcard imports, because you're no longer in sole control of the local namespace.

If you choose to let a dependency inject symbols into your local namespace, and that dependency is the standard library, then you're signing yourself up to have every toolchain upgrade be painful.

Think of it as similar to naming C/C++ types with leading underscores. Yes, you could have gotten away with `typedef int _Bool;` for decades, but eventually the standard library will call that bet.

  > Steve specifically said “unsound”, which means something specific. He
  > didn’t say “doing things outside the bounds of what the Rust change
  > policy guarantees”.
It seems like you're interpreting "unsound" in a sense similar to "violates type-safety soundness", but I don't think that's what Steve meant and it doesn't seem like a reasonable interpretation.


> Think of it as similar to naming C/C++ types with leading underscores. Yes, you could have gotten away with `typedef int _Bool;` for decades, but eventually the standard library will call that bet.

_Bool and all other names beginning with an underscore followed by a capital letter are explicitly reserved by the C++ standard.

Thus writing “typedef int _Bool” in user code is in fact unsound.

> I don’t think that’s what Steve meant

It’s actually quite clear what he meant, because “unsound” is a term of art in the Rust community and usually means something like “code that can be used in a way that invokes undefined behavior”. His explanation elsewhere in this thread of what he meant agrees with my interpretation, not yours.


In C23 bool is a proper type now.


I’m not sure why you’re downvoted here. While it’s true I mentioned unsoundness in my comment, when talking about this I do mean it in the senses you are talking about. It’s just that, as you correctly identified in your first reply, the original assertion is that most programs do not compile, and that specific example is the most straightforward way to acknowledge “yes, it is true that there are non-zero programs which compiled in 1.0 that do not compile today.” Soundness issues aren’t the only example, as I alluded to in my other reply done there, just the easiest one to acknowledge that it’s non-zero. But as also repeated above by both myself and burntsushi at least, the team doesn’t take a “well we said we could” approach to breakage; if breakage would be significant, the rust project either does it another way or doesn’t do it. This means that I believe, as you say, that the original assertion isn’t right. Most programs do still work.

I didn’t get to it in my other comment, but I appreciate your work clarifying in this thread previously; I didn’t see I was mentioned until well after you had made some comments. You did explain the gist of what I was getting at. I wish people wouldn’t suddenly start downvoting someone because they believe you’re wrong all of a sudden.


When I wrote the comment being discussed here, I was thinking of the sibling RFC to the one burntsushi linked above, the one for the language instead of for the library: https://rust-lang.github.io/rfcs/1122-language-semver.html

> This RFC proposes that minor releases may only contain breaking changes that fix compiler bugs or other type-system issues. Primarily, this means soundness issues where "innocent" code can cause undefined behavior (in the technical sense), but it also covers cases like compiler bugs and tightening up the semantics of "underspecified" parts of the language (more details below).

But more importantly, the next paragraph:

> However, simply landing all breaking changes immediately could be very disruptive to the ecosystem. Therefore, the RFC also proposes specific measures to mitigate the impact of breaking changes, and some criteria when those measures might be appropriate.

This has been invoked before; early on in Rust's life, https://rust-lang.github.io/rfcs/1214-projections-lifetimes-... fixed a soundness bug, and as it says

> Warnings first, errors later. Although the changes described in this RFC are necessary for soundness (and many of them are straight-up bugfixes), there is some impact on existing code. Therefore the plan is to first issue warnings for a release cycle and then transition to hard errors, so as to ease the migration.

While the RFC says "a release cycle," that was optimistic. Version 1.4.0 (2015-10-29) introduced the warnings, but the warnings didn't turn into errors until Version 1.7.0 (2016-03-03).

This was intentional breakage that did break programs, for good reason. Those are programs that could have compiled on Rust 1.0 but deliberately do not past 1.7.0.

While it is also true that there are tiny things that technically would break code in many releases, these are only allowed because they're very small things that measurably do not have an impact. They're theoretically breaking, or would be breaking in a different universe where people had written different code that relied on it. Or that is considered acceptable via the API evolution RFC.

But in practice, those changes just aren't experienced by Rust programmers as "breakage." In the 2021 survey, only 2% of users disagreed with the statement "I can upgrade the stable compiler version without fear of my code failing to compile." That isn't even saying "Did it break your code"; it's talking about how users feel about this. 2%! If you go with the closer question, "Upgrading to a new stable compiler version requires either no changes or extremely small and easy changes to my code," only 1% disagree. The data provided doesn't provide more granularity than a percent, so I don't know how it's rounded. But in terms of "is the Rust Project causing users pain with breaking changes?" the answer is very clearly "no."


Are there any programming languages which consider adding new functions to the std library a breaking change?? I know it technically is, but it seems to me that this is ignored everywhere, not just in Rust.(because the chance of breakage is small, and you wouldn't be able to make much in the way of meaningful changes otherwise).


In C++ it should only happen if you do “using namespace std;”, which is basically explicitly opting in to this issue.


Here's the thing. Your rules lawyering doesn't matter. What matters is whether those changes are disruptive enough that people stop using Rust.


Do you have an example where adding a function to the std library broke existing code? I wasn't aware this was possible


The canonical example is when a library has extended a standard trait -- adding a new trait method can cause errors due to ambiguity.

Say you had the following code:

  mod rs_std {
      pub trait StdGreetings {
          fn hello(&self) { println!("Hello!"); }
      }
  }

  use rs_std::StdGreetings;

  trait MyGreetings: rs_std::StdGreetings {
      fn good_morning(&self) { println!("Good morning!"); }
  }

  struct MyGreeter;

  impl StdGreetings for MyGreeter {}
  impl MyGreetings for MyGreeter {}

  fn main() {
      let g = MyGreeter{};
      g.hello();
      g.good_morning();
  }
This is totally valid, it'll compile, and it's not an unreasonable use case to want to extend standard traits like `io::Write`.

The problem is that a newer version of the standard library might have this:

  pub trait StdGreetings {
      fn hello(&self) { println!("Hello!"); }
      
      fn good_morning(&self) { println!("Good morning!"); }
  }
And now that existing code will fail to compile.

The Rust team's answer to this is "crater runs", where they build the current versions of all packages on crates.io with the new compiler to predict the ecosystem impact of a potential change.


> The Rust team's answer to this is "crater runs", where they build the current versions of all packages on crates.io with the new compiler to predict the ecosystem impact of a potential change.

Wow! I never knew this. What a fantastic use case for good, automatic, CI.


https://github.com/rust-lang/rust/issues/88967: they've delayed adding an `Iterator::intersperse()` method to the standard library, since it conflicts with the `Itertools::intersperse()` method from the popular `itertools` crate.


It is possible. If you implement a trait with a function “foo” on some stdlib type, then “foo” is added to the type’s inherent impl, the inherent function will now be called instead of the one in your trait when doing x.foo()


I think it’s rather a pragmatic approach. Fixing that behavior breaks some patterns for which the language doesn’t yet offer reasonable alternatives.


It seems like they’re actually doing it properly and the alternate is the real priority inversion. The customers/users are the goal.


Is this similar to what the keyword generics initiative wants to accomplish as well? They're using an algebraic effects like system however. It seems they want to be generic over certain keywords like async and const but there might be a way to be generic over mut as well, so basically a form of higher kinded types for borrowable and clonable structures.

https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-ge...


I read this post and I think I understand it, but I'm not really sure, so, a question: is there a way to unsafely accomplish compiling the early example by conjuring lifetimes from the aether? Put another way, does the line that fails to compile fail because of some aspect of the concrete lifetime of the values involved, or is it truly an expressiveness problem that doesn't affect the simple type case (i64) because it's Copy (and thus has no lifetime)?


Yes, you can use raw pointers. Example at https://play.rust-lang.org/?version=stable&mode=debug&editio.... The gist is

    trait TryConvertValue: Sized {
        /// SAFETY: `value` must be dereferencable.
        unsafe fn try_convert_value(value: *const Value) -> Option<Self>;
    } 
Now `TryConvertValue` has no lifetime so you don't get the HRTB issues presented in the rest of the article. The unsafeness is that if you call `try_convert_value(null())` or with a dead raw pointer you will cause UB.

One issue with raw pointers though is that they don't extend to lifetime-generic types: there's no raw-pointer equivalent for

    #[derive(Debug)]
    enum Value<'a> {
        String(&'a str),
        Number(&'a i64),
    }

    trait TryConvertValue<'a>: Sized {
        fn try_convert_value(value: Value<'a>) -> Option<Self>;
    }
I wish there was a feature where you could use raw pointers in generic argument position, like `Value<>` which would have variants `String(const str)`, `Number(*const i64)`, but I haven't heard anyone else suggest or advocate for it (`Value<'static>` is not the same)


Whenever I read an article like this about Rust, I thank my lucky stars I don’t have to work with it on a daily basis. Same for C++. It seems like so much effort is wasted just trying to appease the compiler that it makes building actual functioning applications a major chore, especially when it comes to library interop.

And yet there are large projects built in Rust, so there apparently are people out there who enjoy what appears to me to be significant suffering…


Rust is excellent if you don't touch the bits that don't lead you anywhere. I'm quite confident that on a daily basis you can quickly learn to avoid the bits where you won't be successful.

I feel quite productive in Rust and the language gives me a lot of joy.


C++ and rust are not really comparable here; C++ is pretty close to most other statically typed languages like Java, TypeScript, etc. So in terms of spending lots of time appeasing the compiler from my experience that's plainly not true. Orders of magnitude more time is spent debugging why your code is wrong despite it compiling. On top of that almost every time it doesn't compile it's a bug in your code; as is the nature of static type checking. C++ is difficult to write because you're navigating a minefield of undefined behavior, expensive copy constructors and manual memory management; not because of compiler errors.

Rust has taken a trade-off here, where you get more cases of "appeasing the compiler" but significantly more bugs caught at compile time. Memory and concurrency bugs can be extremely hard to debug or even reproduce, so a language that lets you avoid most of those is a positive for productivity.


In real programs this whole thing is avoided by adding a single boolean or a second method.

This article isn't an example of what anyone should be doing, but an exploration of how one can stubbornly push the language to the limit to work around a missing feature. It's Rust's equivalent of preprocessor abuse.


Actually I keep using both for hobby coding, and muss confess I rather lean on C++ side even with its C inherited unsafety due to stuff like this, and the ecosystem naturally.


Appeasing the compiler is the wrong way to think about what you're doing. Think of it instead as proving that your code doesn't contain any memory corruption bugs.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: