[HN Gopher] An incoherent Rust
       ___________________________________________________________________
        
       An incoherent Rust
        
       Author : emschwartz
       Score  : 79 points
       Date   : 2026-03-23 15:13 UTC (7 hours ago)
        
 (HTM) web link (www.boxyuwu.blog)
 (TXT) w3m dump (www.boxyuwu.blog)
        
       | dathinab wrote:
       | This isn't a new discussion it was there around the early rust
       | days too.
       | 
       | And IMHO coherence and orphan rules have majorly contributed to
       | the quality of the eco system.
        
         | MeetingsBrowser wrote:
         | can you elaborate on how have they contributed to the quality
         | of the ecosystem?
        
           | dathinab wrote:
           | there is no good way to handle colliding implementations.
           | Both from parallel crates and due to changes over time.
           | 
           | Without it you can have many many additional forms of
           | breakage. Worse you can have "new" breakage between two 3rd
           | party crates without either of them changing due to some impl
           | in a common ancestor changing (e.g. std) and this affecting
           | two wild card implementations in each, now leading to an
           | overlap.
           | 
           | When you have an overlap there are two options:
           | 
           | - fail compilation, but as mentioned this could be caused by
           | a non breaking change in std in two in theory unrelated 3rd
           | party dependencies
           | 
           | - try to choose one of the implementations. But that now gets
           | very messy in multiple points: a) Which impl. to choose when.
           | b) The user knowing which is chosen. c) Overlap with
           | interactions with stuff like double dispatch, thread local
           | variables, and in general side effects. The issues here are
           | similar to specialization (and part why that is stuck in
           | limbo), but a magnitude more complex as specialization is
           | only (meant) for optimizations, while this can be deeply
           | different behavior. Like `foo.bar()` with the same `use Bar
           | as _;` might in one context return an `u32` and in another a
           | `String`
           | 
           | In many other ecosystems it's not uncommon to run into having
           | issues where certain libraries can't be used together at all.
           | In rust that is close to not a thing (no_mange collisions and
           | C dependencies are the only exception I can think of).
           | 
           | Similar, in my experience the likely hood of running into
           | unintended breaking changes is lower in the rust ecosystem
           | then e.g. python or js, that is partially due to coherence
           | rules forcing a more clean design.
           | 
           | Also people are forced to have a somewhat clean dependency
           | tree between crates in ways not all languages requires. This
           | can help with incremental builds and compiler time, a area
           | rust needs any help it can get. (As a side note, clean
           | dependency structures in your modules can (sometimes) help
           | will rust better parallelizing code gen, too.)
           | 
           | So overall it I think it's good.
           | 
           | Through it can be very annoying. And there is some potential
           | for improvement in many ways.
           | 
           | ---
           | 
           | EDIT: sorry some keyboard fat-fingering somehow submitted a
           | half written response without me pressing enter...
           | 
           | EDIT 2: Fix spelling and sentence structure.
        
       | ekidd wrote:
       | There's a well-known (and frequently encouraged) workaround for
       | the orphan rule: Create a wrapper type.
       | 
       | Let's say you have one library with:                   pub struct
       | TypeWithSomeSerialization { /* public fields here */ }
       | 
       | And you want to define a custom serialization. In this case, you
       | can write:                   pub struct
       | TypeWithDifferentSerialization(TypeWithSomeSerialization)
       | 
       | Then you just implement Serialize and Deserialize for
       | TypeWithDifferentSerialization.
       | 
       | This cover most occasional cases where you need to work around
       | the orphan rule. And semantically, it's pretty reasonable: If a
       | type _behaves_ differently, then it really isn 't the same type.
       | 
       | The alternative is to have a situation where you have library A
       | define a data type, library B define an interface, and library C
       | implement the interface from B for the type from A. Very few
       | languages actually allow this, because you run into the problem
       | where library D tries to do the same thing library C did, but
       | does it differently. There are workarounds, but they add
       | complexity and confusion, which may not be worth it.
        
         | dhosek wrote:
         | The gotcha is what happens when TypeWithSomeSerialization is
         | not something you're using directly but is contained within
         | SomeOtherTypeWithSomeSerialization which you are using
         | directly. Then things get messy.
        
       | nmilo wrote:
       | I will never stop hating on the orphan rule, a perfect summary of
       | what's behind a lot of rust decisions. Purism and perfectionism
       | at the cost of making a useful language, no better way to torpedo
       | your ecosystem and make adding dependencies really annoying for
       | no reason. Like not even a --dangerously-disable-the-orphan-rule,
       | just no concessions here.
        
         | irishcoffee wrote:
         | Go: error handling stinks. Generics would be dope.
         | 
         | Rust: if you spent 3 weeks understanding the syntax and borrow-
         | checker, here are all of the other problems, and the list keeps
         | growing.
         | 
         | Man this cracks me up.
        
         | voxl wrote:
         | Good for better, better for us. Rust is choke full of hard
         | compromises and reactionary subcultures. Just recalling ?
         | alone.
        
         | simonask wrote:
         | I think there are legitimate criticisms of Rust that fall in
         | this category, but the orphan rule ain't it.
         | 
         | In most other languages, it is simply not possible to "add" an
         | interface to a class you don't own. Rust let's you do that if
         | you own either the type or or the interface. That's strictly
         | more permissive than the competition.
         | 
         | The reasons those other languages have for not letting you add
         | your interface to foreign types, or extend them with new
         | members, are exactly the same reasons that Rust has the orphan
         | rule.
        
       | grougnax wrote:
       | If you think Rust has problems, it is that you've have not
       | understood well Rust.
        
         | nmilo wrote:
         | Brilliant
        
       | nixpulvis wrote:
       | I don't think explicit naming of impls is wise. They will
       | regularly be TraitImpl or similar and add no real value. If you
       | want to distinguish traits, perhaps force them to be within
       | separate modules and use mod_a::mod_b::<Trait for Type> syntax.
       | 
       | > An interesting outcome of removing coherence and having trait
       | bound parameters is that there becomes a meaningful difference
       | between having a trait bound on an impl or on a struct:
       | 
       | This seems unfortunate to me.
        
         | wtemple wrote:
         | I don't think fully-qualified paths are enough on their own.
         | You also need some way to designate that an impl is
         | symbolically unique and has to be referred to by path.
         | Otherwise, you still end up with a problem where the compiler
         | doesn't know which implementation to use unless you precisely
         | name it.
         | 
         | You depend on crates A and B. A impls Foo for Bar. You pass an
         | instance of Bar to a function that accepts `impl Foo`. You are
         | happy. Later crate B adds an impl of Foo for Bar. Clearly _at
         | least_ one of these must be an orphan impl, but both could be.
         | Suddenly it's ambiguous which implementation of Foo you're
         | talking about, so you break because B added an impl.
         | 
         | There are many potential problems of this flavor with letting
         | any `impl Trait for Type` be an orphan impl and then referenced
         | by path. What happens, for example, if an impl that was an
         | orphan impl in one version of A becomes a coherent impl in a
         | later version of A?
         | 
         | I think there has to be special syntax for named/path-
         | referenced/symbolic impls, even if the impl does not have an
         | identifier name, so that the compiler can know "this impl only
         | resolves if you tell me _specifically this impl_" and the impl
         | provider has a way to create a solid consumer contract about
         | how to use that impl in particular.
         | 
         | Also, not having an identifier name would mean you can't have
         | different impls of Foo for Bar in the same module. That's
         | probably not a limitation anyone would care about, but it's
         | there.
        
       | Animats wrote:
       | Note the use case - someone wants to have the ability to replace
       | a base-level crate such as serde.
       | 
       | When something near the bottom needs work, should there be a
       | process for fixing it, which is a people problem? Or should there
       | be a mechanism for bypassing it, which is a technical solution to
       | a people problem? This is one of the curses of open source. The
       | first approach means that there will be confrontations which must
       | be resolved. The second means a proliferation of very similar
       | packages.
       | 
       | This is part of the life cycle of an open source language. Early
       | on, you don't have enough packages to get anything done, and are
       | grateful that someone took the time to code something. Then it
       | becomes clear that the early packages lacked something, and
       | additional packages appear. Over time, you're drowning in cruft.
       | In a previous posting, I mentioned ten years of getting a single
       | standard ISO 8601 date parser adopted, instead of six packages
       | with different bugs. Someone else went through the same exercise
       | with Javascript.
       | 
       | Go tends to take the first approach, while Python takes the
       | second. One of Go's strengths is that most of the core packages
       | are maintained and used internally by Google. So you know they've
       | been well-exercised.
       | 
       | Between Github and AI, it's all too easy to create minor variants
       | of packages. Plus we now have package supply chain attacks.
       | Curation has thus become more important. At this point in
       | history, it's probably good to push towards the first approach.
        
         | tekacs wrote:
         | This is interesting but I wonder if you would accept that this
         | also has the downside of moving at the speed of humans.
         | 
         | In a situation where you're building, I find the orphan rule
         | frustrating because you can be stuck in a situation where you
         | are unable to help yourself without forking half of the crates
         | in the ecosystem.
         | 
         | Looking for improvements upstream, even with the absolute best
         | solutions for option 1, has the fundamental downside that you
         | can't unstick yourself.
        
           | tekacs wrote:
           | This is also where I find it surprising that this article
           | doesn't mention Scala at all. There are MANY UX/DX challenges
           | with the implicit and witness system in Scala, so I would
           | never guess suggest it directly, but never have I felt more
           | enabled to solve my own problems in a language (and yes the
           | absolute most complex, Haskell-in-Scala libraries can
           | absolutely an impediment to this).
           | 
           | With AI this pace difference is even more noticeable.
           | 
           | I do think that the way that Scala approaches this by using
           | imports historically was quite interesting. Using a use
           | statement to bring a trait definition into scope isn't
           | discussed in any of these proposals I think?
        
             | kelnos wrote:
             | That was my first thought! I never had this problem with
             | Scala (2.x for me, but I guess there's similar
             | syntax/concepts in 3).
             | 
             | The article author does talk about naming trait impls and
             | how to use them at call sites, but never seems to consider
             | the idea that you could _import_ a trait impl and use it
             | everywhere within that scope, without extra onerous syntax.
             | 
             | Does this still solve the "HashMap" problem though? I guess
             | it depends on when the named impl "binds". E.g. the named
             | Hash impl would have to bind to the HashMap itself at
             | creation, not at calls to `insert()` or `get()`. Which...
             | seems like a reasonable thing?
        
             | tadfisher wrote:
             | The problem is existentials, or rather the existence of
             | existentials without the ability to explicitly override
             | them. Even in Haskell, overriding typeclass instances
             | requires turning off orphan checks, which is a rather large
             | hammer.
             | 
             | So once you've identified this, now you might consider the
             | universe of possible solutions to the problem. One of those
             | solutions might be removing existentials from your
             | language; think about how Scala would work if implicits
             | were removed (I haven't used Scala 3, maybe this
             | happened?). Another solution might be to decouple the whole
             | concept of "existential implementations of typed extension
             | points" from libraries (or crates, or however you compile
             | and distribute code), and require bringing instances into
             | scope via imports or similar.
             | 
             | Two things are true for sure, though: libraries already
             | depend on the current behavior, whether that makes sense or
             | not; and forcing users to understand coherence (which
             | instance is used by which code) is almost always a giant
             | impediment to getting users to like your language. Hence,
             | "orphan rules", and why everyone hates Scala 2 implicits.
        
         | kelnos wrote:
         | > _When something near the bottom needs work, should there be a
         | process for fixing it, which is a people problem? Or should
         | there be a mechanism for bypassing it, which is a technical
         | solution to a people problem?_
         | 
         | I don't think it's a people problem in the way we usually talk
         | about the folly of creating technical solutions to people
         | problems.
         | 
         | If something like serde is foundational, you simply can't
         | radically change it without causing problems for lots and lots
         | of people. That's a technical problem, not a people problem,
         | even if serde _needs_ radical change in order to evolve in the
         | ways it needs to.
         | 
         | But sure, ok, let's imagine that wasn't the case. Let's say
         | some new group of people decide that serde is lacking in some
         | serious way, and they want to implement their changes. They can
         | even do so without breaking compatibility with existing users
         | of the crate. But the serde maintainers don't see the same
         | problems; in fact, they believe that what this new group wants
         | to do will actively cause more problems.
         | 
         | Neither group of people even needs to be right or wrong. Maybe
         | both ways have pluses and minuses, and choosing just depends on
         | what trade offs you value more. Neither group is wrong about
         | wanting to either keep the status quo or make changes.
         | 
         | This _is_ actually a technical problem: we need to find a way
         | to allow both approaches coexist, without causing a ton of work
         | for everyone else.
         | 
         | And even if we _do_ run into situations where things need
         | fixing, and things not getting fixed _is_ a people problem, I
         | 'd argue for this particular sort of thing it's not only
         | appropriate but essential that we have technical solutions to
         | bypass the people problems. I mean, c'mon. People are people.
         | People are going to be stubborn and not want change.
         | Ossification is a real thing, and I think it's a rare
         | project/organization that's able to avoid it. Sure, we could
         | refuse to use technical workarounds when it's people we need to
         | change, but in so many cases, that's just running up against a
         | brick wall, over and over. Why do that to ourselves? Life is
         | too short.
         | 
         | Having said that, I totally agree that there are situations
         | where technical workarounds to people problems can be
         | incredibly counter-productive, and cause more problems than
         | they solve (like, "instead of expecting people to actually
         | parent their kids, force everyone to give up their privacy for
         | mandatory age verification; think of the children!"). But I
         | don't think this is one of them.
        
       | kelnos wrote:
       | This is one of the (several?) things that make me very worried
       | about Rust long-term. I love the language, and reach for it even
       | when it sometimes isn't the most appropriate thing. But reading
       | some of the made-up syntax in the "Removing Coherence" section
       | makes my head hurt.
       | 
       | When I used to write Scala, I accepted the fact that I don't have
       | a background in type/set/etc. theory, and that there were some
       | facets of the language that I'd probably never understand, and
       | some code that others had written that I'd probably never
       | understand.
       | 
       | With a language like Rust, I feel like we're getting there.
       | Certain GAT syntxes sometimes take some time for me to wrap my
       | head around when I encounter them. Rust feels like it shouldn't
       | be a language where you need to have some serious credentials to
       | be able to understand all its features and syntax.
       | 
       | On the other end we have Go, which was explicitly designed to be
       | easy to learn (and, unrelatedly, I don't like for quite a few
       | reasons). But I was hoping that we could have a middle ground
       | here, and that Rust could be a fully-graspable systems-level
       | language.
       | 
       | Then again, for more comparison, I haven't used C++ since before
       | they added lambdas. I wonder if C++ has some hairy concepts and
       | syntax today on par with Rust's more difficult parts.
        
         | maccard wrote:
         | > I wonder if C++ has some hairy concepts and syntax today
         | 
         | https://tartanllama.xyz/posts/cpp-initialization-is-bonkers/
        
         | fridder wrote:
         | I wonder how Zig compares here
        
       ___________________________________________________________________
       (page generated 2026-03-23 23:00 UTC)