3 unstable releases
0.2.0 | Jan 9, 2023 |
---|---|
0.1.1 | Jan 3, 2023 |
0.1.0 | Jan 2, 2023 |
#2589 in Rust patterns
Used in im-rope
29KB
393 lines
static-cow
This Rust crate provides a framework of traits for writing types that are
generic over ownership of their contents, by lifting Cow
to the type level so
that whether a particular object is borrowed or owned can be specified through a
generic type parameter.
Documentation
See API docs on docs.rs.
License
This project licensed under the Apache License
2.0 with LLVM
exception. Unless you explicitly
state otherwise, any contribution intentionally submitted for inclusion in
static-cow
by you, shall be licensed as Apache 2.0 with LLVM exception,
without any additional terms or conditions.
lib.rs
:
This crate provides a framework of traits for writing types that are generic over ownership of their contents.
API Overview
ToOwning
and IntoOwning
ToOwning
and IntoOwning
are the most general traits provided by this
crate, and are the ones that you will implement on your own types.
ToOwning
is a generalization of std::borrow::ToOwned
:
pub trait ToOwning {
type Owning;
fn to_owning(&self) -> Self::Owning;
}
Unlike ToOwned
, it doesn't require that Owning: Borrow<Self>
. Hence
ToOwning
represents a type that can be converted into some version of
itself which owns its contents, but which does not necessarily let you get
a reference to the original borrowing type back out from the owning one.
ToOwning
has a blanket implementation for T where T : ToOwned + ?Sized
.
The blanket implementation does the obvious thing of letting Owning = Owned
and to_owning = to_owned
.
IntoOwning
, then is self-explanatory from its declaration:
pub trait IntoOwning ToOwning + Sized {
fn into_owning(self) -> Self::Owning;
}
IntoOwning
has a blanket implementation for T where T : Clone
, which
makes into_owning
the identity function. Therefore, if your type already
implements Clone
, you get an IntoOwning
implementation automatically.
If you implement IntoOwning
manually, you cannot implement Clone
.
User-defined types which implement ToOwning
and IntoOwning
generally
should just call .to_owning()
and .into_owning()
on each of their
fields. Eventually there will be derive macros for this, but I haven't
written them yet.
StaticCow
StaticCow
, this crate's namesake, is std::borrow::Cow
lifted to
the type level. While Cow
is an enum, StaticCow
is a trait. While
Cow::Borrowed
and Cow::Owned
are enum variants, this crate's
Borrowed
and Owned
are tuple structs which implement StaticCow
(so
also does Cow
). So instead of having a struct with a field field: Cow<'a, B>
, you can declare that field as field: S
and let S
be a generic
parameter S: StaticCow<B>
. Then, wherever the ownedness of S
is known at
compile-time, the compiler can generate an appropriately-specialized version
of the function.
Like Cow
, StaticCow
requires B : ToOwned
, which allows it to have
Deref<Target=B>
for a supertrait. IntoOwning
is another supertrait of
StaticCow
.
Idempotent
Using Idempotent
as a bound allows you to be generic over types that
implement IntoOwning
but not ToOwned
.
StaticCow
<B>
has Deref
<Target=B>
as a supertrait, so you can do
anything with a StaticCow<B>
that you can do with a &B
. However, in
order to provide this supertrait, its implementations require that B : ToOwned
so that they can rely on having B::Owned : Borrow<B>
.
Idempotent
has weaker requirements, so its capabilities are necessarily
weaker as well, and it does not inherit from Deref
. ToOwning
places no constraints on
Owning, which means that as far as the type system is concerned,
.into_owning()is just a completely arbitrary conversion. So, you can't do anything useful with a type that might be
Tor might be
T::Owning` but you don't know which, because they don't promise to have any
traits in common.
Idempotent
puts back just enough information that it can be a useful
bound:
-
It can give you either a
T
or aT::Owning
, and tells you which. -
It constrains
T
such thatT::Owning::Owning = T::Owning
. This means that you can callinto_owning()
on it as many times as you please and it can still give you either aT
or aT::Owning
.
Idempotent<T>
is implemented by Change
<T>
, which holds a T
;
Keep
<T>
, which holds a T::Owning
; and by ChangeOrKeep
<T>
which
might hold either, determined at runtime. Calling .to_owning()
or
.into_owning()
on an Idempotent<T>
always gives a Keep<T>
.
Example
In this example, we'll implement a slice iterator which returns the slice's
elements in reverse. Initially, it'll borrow the slice and clone its
elements when returning them. But, it will implement IntoOwning
, so that
at any time during iteration you can change it into an iterator which owns a
Vec
. It will then pop the elements it returns off the
end of the Vec
, without cloning them.
For starters, we'll declare our flexible iterator:
struct FlexIter<S, E> {
inner: S,
index: usize,
_phantom: PhantomData<[E]>,
}
E
is the type of the slice's elements. And although the constraint doesn't
appear in the struct declaration, S
will be an implementation of
StaticCow<[E]>
. Concretely, S
will be either Borrowed<'b, [E]>
, which
wraps a &'b [E]
, or it will be Owned<[E]>
, which wraps a Vec<E>
.
index
is one greater than the index of the next element we'll return, and
_phantom
is a zero-sized object which has to be there to satisfy the
typechecker by having the parameter E
appear somewhere in the struct's
fields.
Now we'll create ToOwning
and IntoOwning
instances for FlexIter
.
impl<S, E> ToOwning for FlexIter<S, E>
where
S: ToOwning,
{
type Owning = FlexIter<S::Owning, E>;
fn to_owning(&self) -> Self::Owning {
FlexIter {
inner: self.inner.to_owning(),
index: self.index.to_owning(),
_phantom: self._phantom.to_owning()
}
}
}
impl<S, E> IntoOwning for FlexIter<S, E>
where
S: IntoOwning,
{
fn into_owning(self) -> Self::Owning {
FlexIter {
inner: self.inner.into_owning(),
index: self.index.into_owning(),
_phantom: self._phantom.into_owning()
}
}
}
You can see that these implementations are complely rote: we give an
Owning
type which is the same as Self
but with S
replaced by
S::Owning
, and to_owning
and into_owning
methods which simply apply
the same method to each of their fields.
Now we give a constructor for a borrowing iterator, which realizes
StaticCow<[E]>
with Borrowed<'b, [E]>
.
impl<'b, E> FlexIter<'b, Borrowed<'b, [E]>, E> {
fn new(slice: &'b [E]) -> FlexIter<'b, Borrowed<'b, [E]>, E> {
FlexIter {
inner: Borrowed(slice),
index: slice.len(),
_phantom: CowPhantom::default(),
}
}
}
And now we can implement Iterator
:
impl<S, E> Iterator for FlexIter<S, E>
where
E: Clone,
S: StaticCow<[E]>,
{
type Item = E;
fn next(&mut self) -> Option<Self::Item> {
// This is here to show that we can also access `inner` generically
// through its `Deref<Target=[E]>` implementation, without having to
// match on `mut_if_owned()`.
assert!(self.index <= self.inner.len());
match self.inner.mut_if_owned() {
// We're borrowing the slice, so we have to work inefficiently
// by cloning its elements before we return them.
MutIfOwned::Const(slice) => {
if self.index == 0 {
None
} else {
self.index -= 1;
Some(slice[self.index].clone())
}
}
// We own the slice as a `Vec`, so we can pop elements off of it
// without cloning.
MutIfOwned::Mut(vec) => {
// It's necessary to make sure we first truncate the vector
// to `index`, because we may have already started iterating
// before `.into_owned()` was called, and this may be our
// first time calling `.next()` since we took ownership. Of
// course we could have had our `into_owned` implementation
// do this instead of doing it here.
vec.truncate(self.index);
let ret = vec.pop()?;
self.index -= 1;
Some(ret)
}
}
}
}
And now let's see it in action:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let mut borrowing_iter = FlexIter::new(numbers.borrow());
println!("Borrowing:");
println!("{}", borrowing_iter.next().unwrap());
println!("{}", borrowing_iter.next().unwrap());
let owning_iter = borrowing_iter.into_owning();
std::mem::drop(numbers);
println!("Owning:");
for item in owning_iter {
println!("{}", item);
}
}
Running this, we get the expected result:
Borrowing:
5
4
Owning:
3
2
1
This example is also available as examples/flex_iter.rs
in the sources of
this crate.