Expand description
This crate provides the transient::Any trait which re-implements the
dynamic typing mechanism provided by std::any::Any to add support for
types with non-'static lifetimes.
§Introduction
The standard library’s Any trait is used to emulate dynamic typing within
Rust, and is extremely useful in cases where implementing a statically typed
solution would be inconvenient, if not impossible. Examples include storing
heterogeneous values in a Vec, or eliminating generic parameters from a
type so that it can be used in object-safe trait methods.
However, a significant limitation of the std::any::Any trait is its 'static
lifetime bound, which prevents it from being used for types containing any
non-'static references. This restriction eliminates many potential use-cases,
and in others it can force users to sacrifice performance by cloning data that
could otherwise be borrowed.
The crate provides a re-implemented Any trait that circumvents this limitation
to allow type-erasure to be applied to transient (i.e. non-'static) types.
This is achieved by modeling a Rust type as decomposable into separate components
for its raw static data and its lifetime parameters, as embodied by the
Static and Transience associated types of the provided Transient trait.
In this implementation, the Static component is used to obtain the unique
TypeId of the type (which the compiler only hands out for T: 'static),
and the Transience is used as a generic parameter on the re-implemented
Any trait to bound the allowable transitions and uphold Rust’s strict
safety guarantees.
§Features
- Near drop-in replacement for
std::any::Anywhen dealing with'statictypes - Familiar extension beyond
std::any::Anywhen dealing with non-'statictypes - Zero run-time cost above that of a standard
dyn Anycast, with all added functionality implemented using the type system - Safely accounts for the nuances of subtyping and variance
- Supports types with any number of generic lifetime parameters with arbitrary variance combinations
- Supports types with any number of generic type parameters
- Provides the
Transientderivemacro to implement theTransienttrait for most types
§Limitations
- Requires a single
unsafetrait to be implemented for types wishing to utilize the crate’s functionality; however, this trait is usually trivial to safely implement, and aderivemacro is provided for common cases - Only
Sizedtypes are supported. Removing this restriction would be trivial, but makes it awkward to name generic types that require their parameters to beT: SizedsinceT::Static: Sizedmust be explicitly stated even whenT: Sizedcan be implied
§Examples
The first step in using this crate is to implement the Transient trait
for a type. Implementations of this trait are provided for many stdlib
types, it can be derived for most custom types, and it is easy to implement
by hand when more flexibility is needed. Implementations for common types
provided by some 3rd party libraries are also available behind eponymous
feature flags (currently only ndarray, pyo3, and numpy, but feel free
to submit an issue/PR requesting others).
In the trivial case of a 'static type with no lifetime parameters, the
transient crate’s Any trait can be used just like that of the standard
library once the Transient trait has been implemented or derived:
use transient::*;
#[derive(Transient, Debug, PartialEq)]
struct Usize(usize);
let orig = Usize(5);
let erased: &dyn Any = &orig;
assert_eq!(TypeId::of::<Usize>(), erased.type_id());
let restored: &Usize = erased.downcast_ref::<Usize>().unwrap();
assert_eq!(restored, &orig);The trick is that the Any trait as used above is actually generic over a
type known as the Transience, which defaults to (); so the relevant line
in the above snippet actually desugars to erased: &'_ dyn Any<()> = &orig.
This form of the Any trait only supports 'static types, just like the
stdlib implementation.
Where it gets interesting is when a type is not 'static, for which the
Any trait can be parameterized by a Transience type. In the case of
a type with a single lifetime parameter, this can simply be one of three types
provided by this crate, Inv, Co, and Contra, which represent the
three flavors of variance a type can have with respect to a lifetime parameter.
While choosing the correct variance would typically be a safety-critical
decision, the valid choices for the variance of a type are bounded by its
implementation of the Transient trait, and the compiler will prevent you
from using a transience that would not be sound.
We will return to the topic of Transience in a bit, but for now lets choose
Inv (invariant) which is the most conservative form of variance that all
(single-lifetime) types can use. To do this, simply replace dyn Any with
dyn Any<Inv> when coercing a Box or reference to the trait object:
use transient::*;
#[derive(Transient, Debug, PartialEq)]
struct UsizeRef<'a>(&'a usize);
let five = 5;
let orig = UsizeRef(&five);
let erased: &dyn Any<Inv> = &orig;
assert!(erased.is::<UsizeRef>());
assert_eq!(TypeId::of::<UsizeRef>(), erased.type_id());
let restored: &UsizeRef = erased.downcast_ref().unwrap();
assert_eq!(restored, &orig);And that’s all it takes! Things get a slightly spicier in more complicated scenarios, but this crate aims to make the process as painless and intuitive as possible while safely providing a high degree of flexibility for all the niche cases you can imagine.
§Overview
§The Any trait
The most important item provided by this crate is the Any trait, which is
modeled after the standard library’s std::any::Any trait. Much like the
stdlib version, this trait typically appears as the opaque dyn Any trait
object that can be downcast back into an original concrete type. The key
difference is that, while the std::any::Any trait is implemented for all
T: 'static, the transient::Any trait is instead implemented for all
T: Transient (as discussed in the next section). The transient::Any
trait is also different in that it has a generic type parameter know as
he Transience (discussed in another upcoming section) which is used to
enable the support for non-'static types that forms the motivation for
this crate.
§The Transient Trait
The Transient trait is an extremely simple, but unsafe trait consisting
only of two associated types:
pub unsafe trait Transient {
type Static: 'static;
type Transience: Transience;
/* provided methods hidden */
}The first associated type Static is referred to as the static type of the
implementing type, and is simply the same type but with its lifetime parameters
replaced by 'static (e.g., a struct S<'a, 'b> would define Static as
S<'static, 'static>). The static type is used to obtain a TypeId that
uniquely identifies the ('static version of the) erased type so that it can
be safely downcast from an opaque trait object to the concrete type. However,
the compiler only assigns TypeIds for 'static types, so any information
about the true lifetime parameters of the Transient type is lost. Another
mechanism is therefore needed to restore this lifetime information so that
the borrow checker can continue to maintain Rust’s safety guarantees.
The second associated type Transience provides this mechanism by capturing
the lifetime (and variance) information that the static type is missing.
To accomplish this, the transient crate provides the Co, Contra and Inv
structs that exhibit the 3 forms of variance for a single lifetime parameter,
which can be then combined in tuples to accommodate types with multiple (or
zero) lifetime parameters. This type plays several key roles in the safety
and flexibility of this crate’s functionality, as will be discussed below.
Implementing this trait for a type, either manually or by using the included derive macro, is the key ingredient to utilizing the functionality of this crate and is discussed in-depth in its documentation.
§The Transience trait
In common language, transience is a noun that can be defined as
the quality or state of being transient. The transient crate adopts this
term throughout its code and documentation to describe the relationship that a
data structure has with the 0 or more lifetimes parameters it depends on, as
codified by the Transience trait. More specifically, this therm refers the
variance of a type with respect to each of its generic lifetime parameters,
which is a fairly niche topic in everyday Rust programming by plays a major
role in the implementation of this crate.
§Transience bounds and transitions
A simplified version of this crate’s functionality could be implemented by
simply allowing a type T: Transient to be cast to, and restored from, a
dyn Any<T::Transient> trait object. This would be sufficient in some cases,
but is has a significant limitation in that two types S and T with
differing Transience types would erase to different trait objects; the
erased types dyn Any<S::Transience> and dyn Any<T::Transience> would
be distinct types that could not be used interchangeably or stored together
in a homogeneous container.
To evade this limitation and provide maximum flexibility, the transient::Any
trait has a bounded blanket implementation that allows a type to erase to
any transience which is more (or equally) conservative than its own. For
example, a type struct S<'long>(&'long i32) that implements Transient
with a Transience of Co<'long> can be erased to dyn Any<_> with any
compatible transience such as Co<'long>, Co<'short>, Inv<'long>, and
Inv<'short>.
§Mixing covariant and contravariant types
As a result of the flexibility discussed above, the following example of
storing covariant and contravariant types in the same Vec is possible:
use transient::*;
struct CoStruct<'a>(&'a i32);
unsafe impl<'a> Transient for CoStruct<'a> {
type Static = CoStruct<'static>;
type Transience = Co<'a>;
}
struct ContraStruct<'a>(fn(&'a i32));
unsafe impl<'a> Transient for ContraStruct<'a> {
type Static = ContraStruct<'static>;
type Transience = Contra<'a>;
}
let value: i32 = 5;
fn func(val: &i32) { dbg!(val); }
// `co` could erase to `dyn Any<Co>`, but also `dyn Any<Inv>`
let co = Box::new(CoStruct(&value));
// `co` could erase to `dyn Any<Contra>`, but also `dyn Any<Inv>`
let contra = Box::new(ContraStruct(func));
// the type annotation coerces both to choose the latter
let erased_vec: Vec<Box<dyn Any<Inv>>> = vec![co, contra];
assert!(erased_vec[0].downcast_ref::<CoStruct>().is_some());
assert!(erased_vec[1].downcast_ref::<ContraStruct>().is_some());Note however, that this technique is not always possible; if you have a
CoStruct<'short> and ContraStruct<'long>, there would be no common
Transience for them to erase to; the first cannot be lengthened to the
'long lifetime due to its covariance, and the second cannot be shortened
to 'short due to its contravariance.
§Mixing types with different numbers of lifetime parameters
Type with more than one lifetime parameter can use a tuple containing a
variance item for each lifetime as their Transience; this is discussed
in-depth in the documentation for the Transience trait. Consider the
following example defining two types, the first with a single lifetime
and the second with two. We will choose invariance for all lifetime
parameters for simplicity, but when it comes to usage, a similar
situation to the “mixed co- and contra-variance “ example above arises;
if we want to use the types together, we need to find a way to erase
both types to a common dyn Any<_> trait object:
use transient::*;
struct OneLifetime<'a>(&'a i32);
unsafe impl<'a> Transient for OneLifetime<'a> {
type Static = OneLifetime<'static>;
type Transience = Inv<'a>;
}
struct TwoLifetimes<'a, 'b>(&'a i32, &'b i32);
unsafe impl<'a, 'b> Transient for TwoLifetimes<'a, 'b> {
type Static = TwoLifetimes<'static, 'static>;
// we use a tuple for the `Transience` that covers both lifetimes
type Transience = (Inv<'a>, Inv<'b>);
}
let (value1, value2) = (5, 7);
// The "natural" choice would be erasing to `dyn Any<Inv>`
let one = Box::new(OneLifetime(&value1));
// The "natural" choice would be erasing to `dyn Any<(Inv, Inv)>`
let two = Box::new(TwoLifetimes(&value1, &value2));
// The trait objects would not be compatible, but `one` can actually erase
// to `dyn Any<(Inv, Inv)>` as well, since adding additional components is
// allowed; so let's do that:
let erased_vec: Vec<Box<dyn Any<(Inv, Inv)>>> = vec![one, two];
assert!(erased_vec[0].downcast_ref::<OneLifetime>().is_some());
assert!(erased_vec[1].downcast_ref::<TwoLifetimes>().is_some());In this example, we actually could have taken the opposite approach instead
by coercing the types to dyn Any<Inv<'a>>, which would implicitly force 'a
and 'b to be equal. In this case that would work since ’a and ’b are indeed
equal:
let (value1, value2) = (5, 7);
let one = Box::new(OneLifetime(&value1));
let two = Box::new(TwoLifetimes(&value1, &value2));
// allowed because 'a == 'b
let erased_vec: Vec<Box<dyn Any<Inv>>> = vec![one, two];
assert!(erased_vec[0].downcast_ref::<OneLifetime>().is_some());
assert!(erased_vec[1].downcast_ref::<TwoLifetimes>().is_some());However if 'a was 'short and 'b was 'long then the invariance would
prevent them from being unified.
Re-exports§
pub use transience::CanRecoverFrom;pub use transience::CanTranscendTo;
Modules§
- any
- Analogue to the
std::anymodule, containing re-implementations ofAnyandTypeIdthat support non-'statictypes alongside re-exports oftype_nameandtype_name_of_val. - tr
- Re-exports the
Transienttrait to enable unambiguously importing it instead of theTransientderive macro. - transience
- Defines the
Transiencetrait as well as theInv,Co, andContrastructs that implement it. This module also defines theCanTranscendToandCanRecoverFromtraits that establish the allowable transitions between transiences.
Structs§
- Co
- Used to declare a covariant relationship between a type and its lifetime parameter.
- Contra
- Used to declare a contravariant relationship between a type and its lifetime parameter.
- Inv
- Used to declare an invariant relationship between a type and its lifetime parameter.
- TypeId
- Thin wrapper for
std::any::TypeId, which represents a globally unique identifier for a type.
Traits§
- Any
- A trait to emulate dynamic typing, modeled after the
std::any::Anytrait with added support for non-'statictypes. - Downcast
- Extension trait defining methods for downcasting the
dyn Any<_>trait object back into a concrete type. - Static
- Safe trait that
'statictypes can implement to get a free blanket impl of theTransienttrait. - Transience
- Marker trait for types used to establish the variance of a type with
respect to each of its lifetime parameters, including
Co,Contra,Inv,Timeless, and tuples combining them. - Transient
- Unsafe trait defining the lifetime-relationships of a potentially non-
'statictype so that it can be safely erased todyn Any. This trait can be safely derived for most types using theTransientderive macro.
Type Aliases§
- Contravariant
- Type alias that can be used in the
Transienceof a type to declare that it is contravariant with respect to aTransienttype parameter. - Covariant
- Type alias that can be used in the
Transienceof a type to declare that it is covariant with respect to aTransienttype parameter. - Invariant
- Type alias that can be used in the
Transienceof a type to declare that it is invariant with respect to aTransienttype parameter. - Lifetime
- Type alias that can be nested within the
Covariant,Contravariant, orInvarianttype to declare the variance of aTransientstruct with respect to a lifetime parameter. - Timeless
- Used as the
Transienceof a type to declare that it is'staticand not dependent on any lifetime parameters.