Orphan Rule
It appears Rust doesn't allow you to override trait implementations (for some reason I'm unaware of, waiting to hear about that!
In short, because it's opening a can of worms.
Let's imagine it were possible, and work through the issues as they present themselves.
Firstly, which trait implementation should be used, then? Keeping to your idea of better error messages, what if my application has two dependencies A and B each of which overrides the error message: which one should be used A's or B's?
There's not really any good way to decide. You could have arbitrary rules -- such as the closest to the top-level, the last dependency, etc... -- but in the end users will keep running into surprises. And violating the Principle of Least Surprise generally means angry users.
Secondly, even if users are happy enough with the selection rules, this is still brittle. Imagine, library B has been improved, and now it only depends on library C if it needs its functionality -- which you can request with a flag. Great, your application is lighter! Bonus point, you're no longer exposed to vulnerabilities in C or its upstream dependencies.
Oh, on the downside, if your application was relying on C's override of some functionality, it's now broken. No big deal, eh?
Thirdly... it only really works with Late Binding. Remember that in Rust, traits are used both for compile-time polymorphism and run-time polymorphism. Injecting trait implementations from downstream libraries back into their upstream dependencies when compiling them -- ie, before compiling said downstream dependencies -- is not going to be a picnic.
The conservative thing, then, is to simply implement the orphan rule, which Rust did. SO much simpler.
Virtual Table & Virtual Pointer
In most mainstream OO languages -- C++, C#, Java, Python -- a polymorphic value embeds type information, often in the form of a virtual pointer, which itself points to a static virtual table.
Overriding the methods of the object is thus simple: they are executed by looking them up in the virtual table via the virtual pointer, thus swapping out the pointer for one which points to a different virtual table will let you control which implementation of each method is called.
In a statically typed language, the creator of the object will decide the object's exact type, which in turn decides which virtual pointer is "installed" in the object.
In dynamically typed languages, such as JavaScript, Python, or Ruby, it's generally possible to monkey-patch an object, by overriding the methods registered in the v-table. In this case, all objects using this v-table, whether created by the overridder, or not, will start using the newly overridden implementation from then on.
Do note that the distinction between static & dynamic here is fairly arbitrary. In theory, a statically typed language could very well allow monkey-patching. It could even do so in a typed manner: enforcing that the method which overrides accepts the same receiver.
There are downsides, of course:
- Even if you authored some code, you're never sure whether your method implementation was called, or some random method written by a random stranger, and nobody else is, in fact. This makes reasoning about code MUCH harder.
- If multiple libraries keep overriding a given method, it's going to be a hot mess.
- You're basically relying on all the developers of your codebase and all its upstream dependencies to "be nice citizens": they need to document the invariants of each overridable method in detail, and never to modify them on patch/minor updates, and those who override those methods need to very carefully make sure they abide by those invariants. In practice? It'll break, hopefully not often, but when it ineluctably does, it'll be a big pain to diagnose.
alias_methodin Ruby? Could that be done in a statically typed sort of language somehow? $\endgroup$givenas one answer already mentions, which are more exactly like the "overridden trait implementation" solution you describe, but it also hasimplicitvariables, which allow actual dependency injection too. I guess these both solve the same problem in parallel ways? I don't know enough about Scala to answer for sure though. $\endgroup$