Hi! I'm trying to figure out some designs, when I found this particular circumstance:
trait SomeTrait {
fn as_ref(&self) -> &usize;
fn as_mut_ref(&mut self) -> &mut usize;
}
struct Element(usize);
impl SomeTrait for Element {
fn as_mut_ref(&mut self) -> &mut usize {
&mut self.0
}
fn as_ref(&self) -> &usize {
&self.0
}
}
impl SomeTrait for &Element {
fn as_mut_ref(&mut self) -> &mut usize {
todo!()
}
fn as_ref(&self) -> &usize {
&self.0
}
}
fn foo2() {
let element = Element(10);
// I want a compile error on this line, beause this will always be an error
let x = (&element).as_mut_ref();
}
When we want to implement a Trait, in some circumstances we can't write all of them, and is good to know some functions are impossible to call in some circumstances.
In this case, we can't call "as_mut_ref", and this is something that I think could be known at compile time, exists a way to force rust to raise a compile error when someone calls "as_mut_ref" from a reference?
The easiest way to get that is to split up the trait, so that types only implement the traits they actually support and there are no "implemented, but unusable" method stubs sitting around.
trait SomeTrait {
fn as_ref(&self) -> &usize;
}
trait SomeTraitMut {
fn as_mut_ref(&mut self) -> &mut usize;
}
struct Element(usize);
impl SomeTrait for Element {
fn as_ref(&self) -> &usize {
&self.0
}
}
impl SomeTraitMut for Element {
fn as_mut_ref(&mut self) -> &mut usize {
&mut self.0
}
}
impl SomeTrait for &Element {
fn as_ref(&self) -> &usize {
&self.0
}
}
fn foo2() {
let element = Element(10);
// no longer compiles.
let x = (&element).as_mut_ref();
}
This pattern is used throughout the stdlib - see for example Borrow vs BorrowMut, as well as AsRef and AsMut.
mm, that option would make very hard to work with Traits, or would need maybe specialization.
SomeTrait would be split in three, Owned, Ref, MutRef, any function designed for SomeTrait would need to be implemented again for the three of them, like any function that works with Owned needs also be copy/paste to Ref and MutRef.
Split one concept on three ones, also makes harder create groups of them, a function that only needs "ref", and we want to make a Vec<Box>, we would not be able to do it....
Maybe other way is split it, trait MutAccess, RefAccess and OwnedAccess, and SomeTrait in some way to specify from there which traits are needed in each case.
That is accurate; however, in many cases there are shortcuts you can take to implement fewer traits:
Not all problem domains need all three ways of expressing an operation. This is more of an option in application design than in library design, as in an app you can easily delete code that you aren't actually calling; in a library, you're generally trying to plan ahead for what callers might do, and are constrained by compatibility goals once you do add something so that you can't easily delete or change it.
It may be possible to provide blanket implementations such that any FooMut type automatically also implements FooRef, so you only need to specify FooRef by hand for types that aren'tFooMut.
It may be that the operation a trait exposes only makes semantic sense for some subset of the three ways you could be accessing its operands.
On the other hand, if you do need all three variants, and if they can't be implemented in terms of each other, and if you want to have compiler errors when you use a type that doesn't expose one of the three operations, then you need to make that information legible to the compiler, even if it inconveniences you as an author. Splitting up the trait is the most straightforward and least surprising way to do that.
In generic contexts you still end up with the caller needing two trait bounds with that approach, if you have a clear dependency like std does where if you have as_mut_ref you also always have as_ref, you can make the latter a super trait:
I suggest trying a few things out and seeing what clicks.
I should also mention that your particular example is essentially the AsRef/AsMut std traits (or possibly the closely related Borrow/Mut traits) mentioned by @derspiny above. Those give you a bunch of free implementations for eg &T and &mut T, so take a look to see if either they can work directly for you, or for reference for your own implementations.
Hi, the functions like "as_ref" do not necessaty means the object it self as ref, it can also means a internal data, so is fine to only access in that way.
The super trait is also a good complement to have a good structure.
@quinedot Is first time I see the "for" keyword (line 26) like that, in which docs is explained?
Also, similar with the "where" statement on line 22, why there is no errors on that statement? we could also interpret it as, "Self do not implements SupportsMut, so let get a compile error on line 22" but instead it compiles and only raises an error when someone uses it. (Link to some docs also would be nice if think is better than explain here).
Bounds that don’t use the item’s parameters or higher-ranked lifetimes are checked when the item is defined. It is an error for such a bound to be false.
In that case you're probably better off with separate traits for the methods as others suggested.
I could see the pattern being accepted in the future, it just needs to check that dyn SomeTrait: SupportsMut (or dyn SubTraitOfSomeTrait: SupportsMut).[1] But it probably doesn't gain you much over separate traits for the methods if it was supported on its own, because I think the natural ways to approach it[2] are:
// If there are no supertrait relationships
trait FullSomeTrait: SupportsMut + SomeTrait {}
impl<T> FullSomeTrait for T
where
T: SupportsMut + SomeTrait
{}
// Use `dyn FullSomeTrait` or `dyn SomeTrait`
// Or if there are
trait SupportsMut: SomeTrait {}
// Use `dyn SupportMut` or `dyn SomeTrait` and also benefit
// from newly stabilized trait upcasting in Rust 1.86+ to be
// able to coerce from the former to the latter without ceremony
Which looks pretty much the same when you have separate traits for the methods.
The challenge here is that you want compile-time checking, and the way that is being accomplished is with a trait bound on a type. But dyn SomeTrait + 'lt is its own singular, distinct type: either it passes the trait bound and you can call the method, or it doesn't and you can't. The answer has to be the same whatever the erased base type may be, as that's a runtime property and the erased base type is unknown at compile time.
So if you want the answer to be "sometimes yes, sometimes no", but still a compile time check, you need (at least) two different dyn ... types to check the bound against.
If we had custom auto-traits or marker-traits incorporated into the language so that
You couldn't add methods or other items to them
They didn't count as dyn principles so you could have dyn Trait + Marker and coerce it to dyn Trait like you can with Send and other auto-traits
It'sdyn-compatible to have a Self: Marker bound like it is for Send etc
The whole point of dyn SomeTrait is to decouple “producer” and “consumer”.
The one who creates dyn SomeTrait have no idea how that object would be used, only that all the functions that consumer may need are inside, somewhere.
The who consumes dyn SomeTrait have no what's inside, it only have one limitation: not to use anything besides official functions list.
Not only consumer and producer can live in different crates, they can even be compiled entirely independently, while creating two separate modules on two separate machines for the funal merge on the user's machine.
And you want compile-time detection… for that to even mean anything at all you have to decide, first, who is supposed to get the compilation error… and why do you believe that said compilation error wouldn't be fundamental violation of the whole construct.
IOW: instead of “getting the best of both words” you would have to produce “the worst of two words”, where either consumer or producer would have to accept that code that should be compileable generates error message for no good reason.