Any trait implementations where the type and trait are not local are:
* Private, and cannot be exported from a crate
* The local trait implementation always overrides any external implementation
That would solve part of the problem right? Only crate libraries that want to offer trait implementations for external traits/types are not possible, but that might be a good thing.
The solution proposed by the author with implicits is quite complex, I can see why it wasn't chosen.
The problem is that, when you're implementing a foreign trait for a foreign type, you usually want that impl to then be visible to a foreign crate. Not just the local crate.
If it were good enough to only have that impl be visible to the local crate, then you could side step this whole problem by defining a local trait, which you can then impl for any type you like.
So maybe we relax the rules a bit such that only the local crate, and crates that it calls, can see the impl. But then what if a third, unrelated crate, depends on that same foreign crate your local crate depends on? We'd need to keep some sort of stack tracking which impls are visible at any given time to make sure that such foreign crate can only see our impl when that foreign crate is used from our local crate. Hmmm... this is starting to look a lot like dynamic scoping.
How about explicitly declaring orphan trait implementations as public (or not) and explicitly importing them (or not). Trait implementations are resolved at the point where a concrete type becomes generic, and the set of available traits can depend on that context.
This isn't exactly trivial, but it avoids coherence problems.
Crate A implements Hash(vA) for T
Crate B implements Hash(vB) for T
Crate C has a global HashSet<T>
Crates A and B can both put their T instance in the C::HashSet. They can do it in their private code. Their Hash overrides any external implementation. The trait is used, but not exported.
C::HashSet now has an inconsistent state. Boom!
Implementations are not exported or public at all: they are used in functions and those functions are exported. For correctness, you want those implementations to be resolved consistently (this is what coherence is). This post gives the example of unioning two sets: you need to know that they're ordered the same way for your algorithm to work.
So the problem isn't that the implementation is public, it's that its used somewhere by a function which is public (or called, transitively, by a public function). For a library, code which is not being used by a public function is dead code, so any impl that is actually used is inherently public.
You might say, okay, well can binaries define orphan impls? The problem here is that we like backward compatibility: when a new impl is added to your dependency, possibly in a point release, it could conflict with your orphan and break you. You could allow users, probably with some ceremony, to opt into orphan impls in binaries, with the caveat that they are accepting that updating any of their dependencies could cause a compilation failure. But that's it: if you allow this in libraries, downstream users could start seeing unsolvable, unpredictable compilation failures as point releases of their dependencies introduce conflicts with orphan impls in other dependencies.
It would still be consistent; everything with my crate resolves `impl Foo for Bar` to what I define, everything with other crate resolves `impl Foo for Bar` to what they defined, and any other crate would have a compilation error because those crates didn't `impl Foo for Bar`.
If I for some reason exported a method like `fn call_bar(foo: Foo) -> Bar` then I think it would use my `impl Foo for Bar` since the source code for the trait impl was within my crate. What happens if instead I export like `fn call_bar<F: Bar>(foo: F) -> Bar)` is probably a bit more up to debate as to whose trait impl should be used; probably whichever crate where F being Foo is originally known.
I think they did say binaries can define ophan impls; and the only way somebody should be able to break your code is by changing the trait definition or deleting the implementing type. Otherwise your implementation would override the changed implementation. This seems fine because even if I locally define `Foo` which lets me to `Foo impl Bar`; if you then delete Bar then my code breaks anyways.
Of course it can change, that's what removal of coherence does.
It seems to me to be a logical impossibility to allow orphan implementations, and allow crate updates, and not have trait implementations changing at the same time. It's a pick-two situation.
Your conclusion is correct. I'm very happy with the two that Rust picked and tired of people pretending that there will be a magical pick three option if we just keep talking about it.
I also think Rust has picked the right default, but I wouldn't mind having an opt in to the other pair of trade-offs. There are traits like `ToSql` that would be mostly harmless. Serde has tricks for customizing `Serialize` on foreign types, and this could be smoother with language support. Not every trait is equivalent to Hash.
Consider Java for example. In Java, interfaces are even more restrictive than traits: only the package which defines the class can implement them for that class, not even the package which defines the interface. But this is fine, because if you want to implement an interface for a foreign class, you create a new class which inherits from it, and it can be used like an instance of the foreign class except it also implements this interface.
In Rust, to the extent this is possible with the new type pattern it’s a lot of cruft. Making this more ergonomic would ease the burden of the orphan rule without giving up on the benefits the orphan rule provides.
I think Rust assumes that trait implementations are the same across the whole program. This avoids problems e.g. with inlining code or passing data structures between crates. I don't believe this is absolutely necessary though.
If your dependency has a security update, how are you going to get that if you copy-paste the code? The thing these dependency managers do well is that they notify you about these types of issues.
.. That said, people need to be very careful about what they add as dependency. Having 1000+ transitive dependencies is just asking for security issues.
We effectively do the copy-paste. Dependencies are manually committed as a subdirectory to a "deps" directory, and the build scripts updated to search for it in that subdirectory.
To update, we simply download the new version we want, replace the code in the subdirectory, do a build, make some tweaks if needed to build scripts and code, and run tests. Once it builds and tests are fine, we commit both the updated dependency and the other changes in one go.
To get notified we use email (ie subscribe to updates) or just manually keep track.
A nice side-effect of this is that it's trivial to study the changes between the old and the new version before committing. While subtle subterfuge would be hard to spot, blatant stuff like including a bitcoin miner or whatever is trivial to catch.
I once, for a short period, maintained web-application that had three different frontend frameworks (angular 1, angular 2, and one other I don't quite remember), and four different javascript builds that had to be run to build the application. Apparently management prioritized the giving demos to investors, and changes to different frameworks were aborted halfway through.
It was completely impossible to work with. Each week the build failed for new random reasons. I hardly dared touch the thing.
This "pattern" is really a failure from multiple parties:
* Managing software engineers is an art, and you really need to understand what is happening to succeed. Only prioritizing short-term goals just ensures you're going to fail in the long-term. Make sure you understand technical debt. The speed of work in bad code-bases versus good code-bases can be orders of magnitude in difference.
* Software engineers really need to use branches properly. Work that is halfway done should not be in the main branch. Consistency and simplicity is king here. Maintaining an old and new version of software for a while can be a pain, but it's much better than maintaining a halfway converted application. Pressure from management is no reason to release stuff halfway done. And if you need to demo, release a specific branch.
Nowadays I don't even ask to do necessary maintenance. It's just part of the job. Always stick to the boyscout rule (leave things in a better state then you found it). Make your code-bases cleaner incrementally, and eventually you'll be in a much better state.
Or have a lower melting point, making it more practical to use some sort of heat-resistant armoring? Might be pretty difficult to make something that both reflects light and is heat-resistant, no mirror reflects everything.
An alternative which I've used with some succes are structured state space models: https://srush.github.io/annotated-s4/. A very different approach that works well for quite a few types of problems.
That's a pretty standard part of MLOps. I have a fraud model in production, it's being incrementally retrained each week, on a sliding window of data for the last x-months.
You can do it "online", which works for some models, but for most need monitoring to make sure they don't go off the rails.
That's good to hear, how does it work in practice? Is it basically running the same training as from scratch, but with only the new data, on a separate machine to produce a new version which is then replacing the old production version? Is part of MLOps starting a new training session each week, checking if the loss function looks ok, and then redeploying it?
I still think of how humans work. We don't get retrained from time to time to improve, we learn continually as we gain experience. It should be doable in at least some cases, like classification where it's easy to tell if a label is right or wrong.
* Take the previous model checkpoint, retrain/finetune it on a window with new data. You typically don't want to retrain everything from scratch, saves time and money. For large models you need specialized GPUs to train them, so typically the training happens separately.
* Check the model statistics in depth. We look at way more statistics then just the loss function.
* Check actual examples of the model in action
* Check the data quality. If the data is bad, then you're just amplifying human mistakes with a model.
* Push it to production, monitor the result
MLOps practice differs from to team to team, this checklist isn't universal, just one possible approach. Everyone does things a little differently.
> I still think of how humans work. We don't get retrained from time to time to improve, we learn continually as we gain experience. It should be doable in at least some cases, like classification where it's easy to tell if a label is right or wrong.
For some models, like fraud, correctness is important. Those models need a lot of babysitting. For humans, think about how the average facebooker reacts to misinformation, you don't want that to happen with your model.
Other models are ok with more passive monitoring, things like recommendation systems.
Continuous online training can be done. Maybe take a look at reinforcement learning? It's not widely applied, has some limitations, but also some interesting applications. These types of things might become more common in the future.
I've applied the S4 operator to successfully do long-length video classification. It's massively more efficient than a similarly scaled transformer, but it doesn't train as well. Still, even with S4 I got some impressive results, looking forward to more.
Any trait implementations where the type and trait are not local are:
* Private, and cannot be exported from a crate
* The local trait implementation always overrides any external implementation
That would solve part of the problem right? Only crate libraries that want to offer trait implementations for external traits/types are not possible, but that might be a good thing.
The solution proposed by the author with implicits is quite complex, I can see why it wasn't chosen.