Imagine some event source, which produces events represented as an enum. Of course, for best efficiency, this producer is zero-copy, i.e. it returns references to its internal buffers:
enum Variant<'a> {
Nothing,
SomeInt(u64),
SomeBytes(&'a [u8])
}
impl Producer {
fn next(&'a mut self) -> Variant<'a> { ... }
}
This is perfectly fine for consumers that don't require lookahead or backtracking, but sometimes there is a need to save some sequence of events. Thus, our Variant
type becomes a generic:
enum Variant<BytesT> {
Nothing,
SomeInt(u64),
SomeBytes(BytesT)
}
type OwnedVariant = Variant<Vec<u8>>;
type BorrowedVariant<'a> = Variant<&'a [u8]>;
Here, we end up with two types with "owner-reference" relationship, which is similar to pairs Vec<T>
-&[T]
, String
-&str
. Docs suggest builtin traits Borrow
and ToOwned
which provide just what is required except for a subtle nuance:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
// this: -----------^
}
pub trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
Result of borrow
is required to be a reference to something, which BorrowedVariant<'a>
is obviously not. Removing this requirement solves this problem (here, names are prefixed with alt to emphasize the fact this is an alternative interface):
trait AltBorrow<'a, AltBorrowed> {
fn alt_borrow(&'a self) -> AltBorrowed;
}
trait AltToOwned<'a> {
type AltOwned: AltBorrow<'a, Self>;
fn alt_to_owned(&'a self) -> Self::AltOwned;
}
This trait could then be implemented for standard types, e.g. Vec
:
impl<'a, T> AltBorrow<'a, &'a [T]> for Vec<T> {
fn alt_borrow(&'a self) -> &'a [T] {
self.as_slice()
}
}
impl<'a, T> AltToOwned<'a> for &'a [T]
where T: Clone
{
type AltOwned = Vec<T>;
fn alt_to_owned(&'a self) -> Vec<T> {
self.to_vec()
}
}
As well as for the Variant
enum in question:
impl<'a> AltBorrow<'a, BorrowedVariant<'a>> for OwnedVariant {
fn alt_borrow(&'a self) -> BorrowedVariant<'a> {
match self {
&Variant::Nothing => Variant::Nothing,
&Variant::SomeInt(value) => Variant::SomeInt(value),
&Variant::SomeBytes(ref value) => Variant::SomeBytes(value.alt_borrow()),
}
}
}
impl<'a> AltToOwned<'a> for BorrowedVariant<'a> {
type AltOwned = OwnedVariant;
fn alt_to_owned(&'a self) -> OwnedVariant {
match self {
&Variant::Nothing => Variant::Nothing,
&Variant::SomeInt(value) => Variant::SomeInt(value),
&Variant::SomeBytes(value) => Variant::SomeBytes(value.alt_to_owned()),
}
}
}
Finally, the questions:
Borrow
/ToOwned
concept? Should I use something else to achieve this?std::borrow
might have been preferred?This example on Rust playpen
The borrow check is Rust's "secret sauce" – it is tasked with enforcing a number of properties: That all variables are initialized before they are used. That you can't move the same value twice. That you can't move a value while it is borrowed.
An Example of Borrowing in Rust You can borrow the ownership of a variable by referencing the owner using the ampersand (&) symbol. Without borrowing by referencing, the program would panic. It would violate the ownership rule that a value can have one owner, and two variables cannot point to the same memory location.
Got some explanation on #rust IRC.
From aturon:
the short answer is: we'd need higher-kinded types (HKT) to do better here; it should be possible to smoothly "upgrade" to HKT later on, though
(this is a pattern that's come up a few places in the standard library)
(lifting the lifetime to the trait level is a way of encoding HKT, but makes it significantly more awkward to use the trait)
From bluss:
I like your question. That kind of lifetime in a trait hasn't been explored enough IMO but it also has a known bug in the borrow checker right now
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With