Its worth remembering that we’re running up against a fundamental limit here - this is the same problem that manifests in other systems as diamond inheritance or the wild results you can get from monkeypatching in dynamic languages. I think our current solution can get better, but I think its impossible to find a perfect solution.
Depends on what you mean by “perfect.” It’s pretty obvious that it’s mathematically impossible to solve this problem in the general case, but I think there’s an optimal solution to any specific case that has any solution.
One advantage is that it allows for multiple custom trait implementations to exist, and for those to all be used on the same type without much hackery by the user. Say we had a type Foo
, and one external library implemented ToString
on Foo
and another external library implemented FromStr
on Foo
. With wrapper types you’d have an unnecessarily difficult time using both of those implementations on the same instance of a type, whereas “named impl blocks” would let them be used simultaneously pretty easily.
Yeah, sorry, I read too much into this proposal without reading it carefully first. What I had in mind was a different kind of a proposal, where a crate may forward declare an impl, and then other crates that depend on that crate may assume that some impl exists, but they – diesel and rocket for example – don't get to decide which one. Finally, the bottom crate – usually a binary – has the power to decide which implementation to use, and the implementing crates are required to only contain the impl. It's not a foolproof system, I guess, but I think it enables a social dynamic where the library authors are encouraged to be careful not to depend on specific impls.
The problem with such a system is that it implicitly encourages there being multiple implementations.
Also, stuff gets complicated when you start dealing with code accepting generic parameters.
Also hard to say you can’t depend on the specific implementation. Different implementations could have semantically significant differences, such as opposite implementations of PartialOrd
.
Just as an aside, this is what Coq does (the naming, at least - it's mandatory, as a matter of fact). I'm unsure of how Coq handles imports of such.
One nasty facet is that Coq allows declaring two such impls in the same place (so long as the names differ) - the last declared is silently used.
As an example, if I implemented Add<Foo> for Foo
twice, and then wrote a function fn frob<F: Add<Foo>>(f: F)
and invoked it with a Foo, it would silently use the second impl, without disambiguation. (I am, in fact, unsure disambiguation is possible without creating a module boundary and importing only one - I ran afoul of this in a project.)
Older versions of Rust had named impls that had to be imported. We backed away from it over time for a variety of reasons.
We ‘support’ named impls today through this very wild hack. If you would like your trait to support named impls, you can do this:
trait ToString<Name: NamedToString<ToString = Self>> {
fn to_string(&self) -> String;
}
trait NamedToString {
type Receiver: ?Sized;
fn to_string(this: &Self::Receiver) -> String;
}
impl<Name> ToString<Name> for Name::Receiver where Name: NamedToString {
fn to_string(&self) -> String {
Name::to_string(self)
}
}
Your clients can provide named impls for foreign traits:
struct ToStringForIntSlice;
impl NamedToString for ToStringForIntSlice {
type Receiver = [i32];
fn to_string(this: &[i32]) -> String {
panic!()
}
}
To be clear: I don’t think anyone should do this.
It doesn't require wrapping the object to get the functionality. In fact, another way of looking at named implementations could be to have a way to make a newtype that can implicitly wrap an object if a method of the implementation is called and there are no other conflicting implicit newtypes in scope (similar to how implicit classes work in scala).
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.