3 releases (breaking)
new 0.3.0 | Jan 7, 2025 |
---|---|
0.2.0 | Sep 22, 2024 |
0.1.0 | Sep 20, 2024 |
#811 in Rust patterns
38KB
398 lines
Supply
Provider API for arbitrary number of lifetimes.
supply
implements an API similar to that proposed in
RFC 3192.
It was proposed to provide Error
and API to expose extra context
information for errors. While the original RFC was a general API, the current implementation in
core
/std
is specifically for Error
. The original proof of concept implementation can be
found at https://github.com/nrc/provide-any.
supply
is a reimagining of the provide API to be more flexable and general purpose.
The motivation section of RFC 3192 states the problem this API is trying to solve very well.
However, in practice some kind of partial abstraction is required, where objects are treated abstractly but can be queried for data only present in a subset of all types which implement the trait interface. In this case there are only bad options: speculatively downcasting to concrete types (inefficient, boilerplatey, and fragile due to breaking abstraction) or adding numerous methods to the trait which might be functionally implemented, typically returning an Option where None means not applicable for the concrete type (boilerplatey, confusing, and leads to poor abstractions).
https://rust-lang.github.io/rfcs/3192-dyno.html#motivation
A similar pattern that is very useful but rarely seen in the wild is that proposed by
gdbstub
.
That of so called Inlineable Dyn Extension Traits (IDETs). You can think of the provide API
as an abstract form of this pattern.
The core idea of the provide pattern is to use an output parameter to receive a type erased value.
You may ask why just returning a type erased value is an issue, and its a good question. The
answer is that returning ownership directly requires something like a Box
. However, we
don't always have access to a box or don't want the extra allocation. But wait what if we just
returned a &dyn Any
? This doesn't need a box. However, this prevents returning owned values.
We are limited to things that can be borrowed from the source.
Instead we take another approach. We construct a "hole". A place a value should go. We then give a borrow of this hole to a value for it to "fill".
This design has some major advantages. For one it solves the returning ownership issue. Instead the provider transfers ownership into the existing hole we gave it. Because as the requester we know the type of data we want we can use some stack space to store the hole and eventual value. Additionally, this design allows a requester to change the behavior of the hole. For example we can have many holes that all need to be filled by the provider in one operation.
Another aspect that supply
expands on over the RFC is the use of arbitrary length lifetime
lists. We won't get into those here. See the ty-tag
crate for
more information about those.
Connecting this theoretical design to supply
's implementation we get the following.
The hole is represented by something implementing the [Want
] trait. This trait has methods
for providing it a value to store. The provider then implements [Provider
] and when it's
methods are called provides any values it can to the passed in want.
A requester uses a provider by first constructing an empty [Want
] implementer. Then, the
requester calls .provide()
on the [Provider
] to give the want a value. Then the requester
can remove the value from the want and use it however it needs to. Using [ProviderExt
] we
can use a simple method
call of the form provider.request::<Request>()
to request values from a provider.
It is recommended to use the prelude which has the common API elements.
use supply::prelude::*;
Most users should start with implementing the [Provider
] trait on one or more
types they want to expose extra information from. Then the .request::<T>()
method
can be used to request a specific type of information from the provider. This
is the core API flow of supply
. Below is an example with a couple of the more advanced
features for demonstration.
Examples
use supply::prelude::*;
struct Person<'a> {
name: &'a str,
age: u8,
}
// Implementing Provider allows requesting data from a Person value.
impl<'r, 'a> Provider<'r> for Person<'a> {
type Lifetimes = l!['a];
fn provide(&'r self, want: &mut dyn Want<Self::Lifetimes>) {
// Provide the name and age fields.
want.provide_value(self.name)
.provide_value(self.age);
}
}
// Make an example person to request data from.
let name = String::from("bob");
let provided_name;
{
let person = Person {
name: &name,
age: 42,
};
// Convert to a trait object to show Provider is object safe.
let provider: &dyn ProviderDyn<l!['_]> = &person;
// Request the person's name.
provided_name = provider.request::<&str>();
assert_eq!(provided_name, Some("bob"));
// Request the person's age.
let provided_age = provider.request::<u8>();
assert_eq!(provided_age, Some(42));
// Request something Person doesn't provide.
// We just don't get a value from the request.
let provided_something = provider.request::<f32>();
assert_eq!(provided_something, None);
};
// Because the name was tagged as &'a str we can still access it here.
assert_eq!(provided_name, Some("bob"));
no_std
Support
This crate is always #![no_std]
, it can be used anywhere Rust can.
Minimum Supported Rust Version
Requires Rust 1.83.0.
This crate follows the "Latest stable Rust" policy. The listed MSRV won't be changed unless needed. However, updating the MSRV anywhere up to the latest stable at time of release is allowed.
Contributing
Contributions in any form (issues, pull requests, etc.) to this project must adhere to Rust's Code of Conduct.
Unless you explicitly state otherwise, any contribution intentionally submitted for
inclusion in supply
by you shall be licensed as below, without any
additional terms or conditions.
License
This project is licensed under either of
at your option.
Dependencies
~55KB