But they are oh not identical at all! Taking the Iter{,Mut}
pair example, see how implementing Iter
is trivial:
struct Iter<'slice> {
slice: &'slice [u8],
cursor: usize,
}
impl<'slice> Iter<'slice> {
fn next (self: &'_ mut Iter<'slice>)
-> Option<&'slice u8>
{
let ret = self.slice.get(self.cursor)?;
self.cursor += 1;
Some(ret)
}
}
and yet when doing the "conceptually-identical" transposition…
struct Iter<'slice> {
- slice: &'slice [u8],
+ slice: &'slice mut [u8],
cursor: usize,
}
impl<'slice> Iter<'slice> {
fn next (self: &'_ mut Iter<'slice>)
- -> Option<&'slice u8>
+ -> Option<&'slice mut u8>
{
- let ret = self.slice.get (self.cursor)?;
+ let ret = self.slice.get_mut(self.cursor)?;
self.cursor += 1;
Some(ret)
}
}
…the code does not compile anymore.
This is because for the shared references case, if one "forgets" to put the self.cursor += 1
line, all they get is a logic error whereby the iterator is infinite and always yields the same element, but it won't lead to Undefined Behavior.
Whereas for the unique references case, if it compiled (e.g., by transmute
-laundering the lifetime of the obtained reference), forgetting the self.cursor += 1
line would lead to unsound code.
in that situation you are pretty much screwed (pardon my language): the very point of APIs as abstraction boundaries, enforced by type-level shenanigans is to force a certain way of using it, and since that does not mean the API itself was well designed, it can definitely lead to unusable APIs .
So it seems to me that the root problem here, the one causing most grievance, is how ill-designed the APIs you are working with seem to be.
Given the "losing" starting position that you have had the misfortune to find yourself into, using runtime-panicking paths to express that the API can be used in more ways than it was intended (mainly, IIUC, the fact that the trait T
can be used without calling g
in some cases) is one of the main ways to circumvent overly strict type-level designs. In other words: using unimplemented!()
may not be pretty, but is actually the most sensible stand-alone out you have at your disposal.
-
Basically, the API author of the trait T
expressed that it was "paramount" for the behavior expressed in T
to feature both f()
and g()
capabilities. Rust trusts that, and does not let you implement f()
only, since "g()
may be called". That is the case even if g()
isn't! So using unimplemented!()
(or unreachable!()
, we are in between those two concepts) is a way to tell Rust: "hey, don't worry, I'm willing to bet the control flow of my program that g()
is actually not called". And Rust is then like "Oh, if you are willing to sacrifice your control flow should you be wrong, then go ahead".
-
If we assume that the trait T
should have been written as:
trait TRef {
fn f(&self);
}
trait TMut : TRef {
fn g(&mut self);
}
use TMut as T; //
-
Examples: Index
{,
Mut
}
, Borrow
{,
Mut
}
, Deref
{,
Mut
}
;
-
Or the unordered version, where TRef
and TMut
are independent of each other, and T
is just an alias for having both, as @2e71828 showcased, with As{
Ref
,
Mut
}
as the standard library example.
then, one way to fix the abstraction from your "weak" downstream user position, is to try and recreate that pattern (still using unimplemented!
or unreachable!
to soothe Rust) with your own custom trait and newtype wrapper:
#[derive(::ref_cast::RefCast)]
#[repr(transparent)]
struct ImplTRef<X : TRef>(X);
impl<X : TRef> T for ImplTRef<X> {
fn f(&self) { self.0.f() }
fn g(&mut self) { unimplemented!("No `&mut`s here") }
}
// where
trait TRef {
fn f(&self);
}
And you can then feed ImplTRef::ref_cast(your_ref)
(where your_ref: &(impl TRef)
is a shared reference to a type that implements TRef
) to APIs that expect an &(impl T)
.
-
If you don't want to be that general, but are fine with using unimplemented!
and just want to reduce the boilerplate, then go for that suggested solution instead.
What I'd personally do, however, is that, since I find bad APIs unforgiveable (see how much grievance it is causing you), is to:
-
fork the repo of the crate with the design issue,
-
fix the issue (e.g., split T
into two traits here),
-
submit a PR so as to hopefully get the fix implemented upstream,
-
use the patch
section of the Cargo.toml
file to use your fork in the meantime.
- should the PR never be accepted, and should the issue be big enough (this API case seems to be one example), then publishing the fork as its own stand-alone crate would be the next logical step.
This way you don't have to use unimplemented!()
, and you have potentially helped future users of that library avoid the issue altogether