Add provide_any module to core - #3192
Conversation
Signed-off-by: Nick Cameron <nrc@ncameron.org>
|
Add link to Rendered version |
|
If it helps at all, this looks like it would be amaaaazing for |
|
|
||
| `provide_any` could live in its own crate, rather than in libcore. However, this would not be useful for `Error`. | ||
|
|
||
| `provide_any` could be a module inside `any` rather than a sibling (it could then be renamed to `provide` or `provider`). |
There was a problem hiding this comment.
Living under any rather than in a new top-level module seems like a more natural home for this to me. There's overlap between type tags and type ids, and Any and Provider that I think we could give proper treatment together under the same umbrella.
There was a problem hiding this comment.
that makes sense! I'm a bit hesitant because type ids and type tags are so obviously similar but have nothing in common in their interfaces or how they are used, so I thought it might be a bit confusing to have them together
There was a problem hiding this comment.
Do you think trying to come up with a module-level doc for a hypothetical combined any module might be a good exercise for identifying those cases where that confusion can come from?
|
|
||
| The `TypeTag` trait is an abstraction over all type tags. It does not have any methods, only an associated type for the type which the tag represents. E.g., `<Ref<T> as TypeTag<'a>>::Type` is `&'a T`. | ||
|
|
||
| There is no universal type tag. A concrete type tag must be written for a 'category' of types. A few common tags are provided in `provide_any::tags`, including `Value` for any type bounded by `'static`, and `Ref` for types of the form `&'a T` where `T: 'static`. For less common types, the user must provide a tag which implements `TypeTag`; in this way the `provide_any` API generalises to all types. |
There was a problem hiding this comment.
Why is Ref not a type tag combinator, where if T has a type tag then Ref can be used as type tag for references to T (somewhat similar to the Option combinator that does seem to exist)?
There was a problem hiding this comment.
My guess is convenience. I think if Ref was implemented the same way Optional is you'd have to invoke it with foo.get_by_tag::<Ref<Value<MyType>>>(). Thinking about it this doesn't seem too bad, since we already have get_context_ref that handles this case, so we may never need to use the Ref type tag directly as is, which seems like a good justification for updating it to compose with other TypeTags.
There was a problem hiding this comment.
One reason is that type tags represent Sized types, so using the compositional form for Ref would mean we lose the ability to represent references to ?Sized types. You can add the ?Sized bound to the Type associated type, but that means a lot of I::Type: Sized bounds all over the place, so it seems sub-optimal.
commented
Nov 15, 2021
|
I found a nice usecase for something like this in my day-to-day codebase, which is a storage engine, that I thought I'd share. Individual documents can be cached for better performance. Caching is fine-grained, so for each document we can choose a different caching strategy. We've got a trait for these cache entries that looks like this: pub trait CacheEntry: Any + Send + Sync + Display {
/**
The raw on-disk payload bytes.
*/
fn bytes_from_disk(&self) -> Option<BytesFromDisk> {
None
}
/**
The payload bytes that are yielded to callers.
This may involve decompression or other transformations over the raw on-disk payload.
*/
fn bytes(&self) -> Option<Bytes> {
None
}
/**
The approximate size of this entry in-memory.
*/
fn approximate_size(&self) -> usize;
}The combination of |
| pub trait TypeTag<'a>: Sized + 'static { | ||
| type Type: 'a; | ||
| } |
There was a problem hiding this comment.
| pub trait TypeTag<'a>: Sized + 'static { | |
| type Type: 'a; | |
| } | |
| pub trait TypeTag: Sized + 'static { | |
| type Type<'a>: 'a; | |
| } |
This could be made nicer with GATs, which should be stabilizing soon.
There was a problem hiding this comment.
This change doesn't seem to make things much nicer, it just moves the lifetime parameter from the type to the associated type, it still needs to be specified in most cases. I think GATs make this harder to understand, and the original is closer to my mental model of a type tag in any case (i.e., the tag includes the lifetime bound of the type it represents as part of its type, rather than the tag represents a type constructor that can be parameterised by any lifetime).
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Signed-off-by: Nick Cameron <nrc@ncameron.org>
commented
Dec 6, 2021
|
RFC updated to take into account comments - thanks for the feedback! |
Signed-off-by: Nick Cameron <nrc@ncameron.org>
commented
Dec 15, 2021
|
One thing that has been floating in my mind is – what's the complexity of a function that provides a huge number of different types, and can this be made to be better than Given the particularities of |
commented
Dec 16, 2021
|
It seems O(n) is the best we can do with this API. However, I don't think n should ever be large, in practice it will be limited by the number of fields (I guess you could be providing a whole bunch of new data, but it seems like you should have a dedicated, named API for that). I couldn't work out a way to have O(1) perf here, though I agree it seems like it should be possible some how. |
Not sure if this is satisfying, but because of the flexibility of the provider API I think you could pretty much always just provide a more performant interface that does have |
Signed-off-by: Nick Cameron <nrc@ncameron.org>
| ```rust | ||
| pub mod provide_any { | ||
| pub trait Provider { | ||
| fn provide<'a>(&'a self, req: &mut Requisition<'a>); |
There was a problem hiding this comment.
Isn't this unsound now (after Requisition<'a, '_> -> &mut Requisition<'a> change)?
Since Requisition is actually similar to (TypeId, Option<T>) for some erased T, this theoretically allows writing a safe_transmute by
- Requesting type
U - In provider replacing request for
Uwith a request forTviamem::swap - In provider satisfying the request with
T-value - Getting
Ufrom the request, but since the request was changed to a request forT, this readsTasU
I couldn't figure out how to get two requests with the same lifetime, but I'm sure there is a way/it's easy to accidentally allow something like this.
It seems like this API should either
- Use
Pin<&mut Requisition<'a>>, like theobject_providercrate - Use
Requisition<'a, '_>and provide a reborrow API to change the'_and allow delegating
There was a problem hiding this comment.
I don't think so because the implementation of request_by_type_tag will use the original Requisition (for U) not the new one (for T) so the user will always get back None.
Note that the old and new types are equivalent, with the lifetimes fully explicit they are Requisition<'a, 'b> and &'b mut Requisition<'a>
There was a problem hiding this comment.
Doesn't request_by_type_tag call provide with a reference to it's Requisition? I imagined it's implemented in a similar facion:
#[repr(C)]
struct Requisition<'a> {
id: TypeId,
ph: PhantomData<&'a ()>,
}
pub fn request_by_type_tag<'a, I: TypeTag<'a>>(provider: &'a dyn Provider) -> Option<I::Type> {
let mut req = (TypeId::of::<I>(), None::<I::Type>); // this should be a `repr(C)` struct, not a tuple
let req = transmute::<_, &mut Requisition<'a>>(&mut req);
provider.provide(req);
req.1
}In this example provider can change the TypeId that Requisition checks, by replacing it with another Requisition with a different TypeId, but the slot stays the same.
If I understand correctly, Requisition<'a, 'b> was wrapping *mut inside and so replacing it wouldn't bring inconsistency between TypeId and the slot.
There was a problem hiding this comment.
Ah, I see what you mean, sorry.
Implementation is here: https://github.com/rust-lang/rust/pull/91970/files#diff-0752889661748b8a15a597d7156127b6fb90fdeda0627be50e07f3f785bd0f4dR798-R805
Requisition never stores the type id, it is synthesised as needed.
Its not actually possible to create a new Requisition since TagValue is private to the any module. In addition, Requisition is unsized so I think that even if you could create one, you can't assign it into the mutable reference.
There was a problem hiding this comment.
Its not actually possible to create a new Requisition since TagValue is private to the any module
If Requisition was Sized, the only thing you'd need would be &mut Requisition, since you could use mem::swap.
Implementation is here: https://github.com/rust-lang/rust/pull/91970/files#diff-0752889661748b8a15a597d7156127b6fb90fdeda0627be50e07f3f785bd0f4dR798-R805
I see. Interesting, thanks!
There was a problem hiding this comment.
If Requisition was Sized, the only thing you'd need would be &mut Requisition, since you could use mem::swap
I don't understand, how would you create the new Requisition to swap in? Requisition has a TagValue field and you need to name that to create it and thus create the Requisition.
There was a problem hiding this comment.
You could do another request_by_type_tag, but I suppose there's no way to get the lifetimes to match up.
commented
Mar 1, 2022
|
I've significantly reworked the RFC and draft implementations (https://github.com/nrc/provide-any and rust-lang/rust#91970). The main change is to remove type tags from the API. This lets us iterate on that part of the proposal without breaking changes. It also means the surface area of the RFC is much smaller. I have removed handling of mutable references for now (though they are still present in https://github.com/nrc/provide-any so you can see the changes requires) to further shrink the surface area. There are some further changes to the implementation to simplify it and provide better encapsulation of the implementation. I think this resolves all open questions, though since the RFC is now somewhat different there may be new ones :-) |
commented
Mar 1, 2022
|
The new version makes much more sense to me! |
Signed-off-by: Nick Cameron <nrc@ncameron.org>
commented
Mar 2, 2022
@joshtriplett are these concerns resolved now that the type tags have been turned into an implementation detail? |
commented
Mar 2, 2022
commented
Mar 2, 2022
|
@rfcbot resolved typetag |
commented
Mar 5, 2022
|
Is it possible to request/provide with an extra const string FIELD_NAME? (may be blocked by const_adt_params) In many cases, there may exists multiple fields with same type in some traits. Although newtype is a good pattern, rust doesn’t has a good support for it now. |
commented
Mar 5, 2022
imho it's better to use a struct type tag, rather than a const str as the disambiguating parameter, since that handles inadvertent typos or name clashes much better. |
commented
Mar 16, 2022
|
🔔 This is now entering its final comment period, as per the review above. 🔔 |
commented
Mar 26, 2022
|
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
commented
Apr 6, 2022
|
when this can be merged? |
commented
Apr 6, 2022
Please note that this is only an RFC, not an implementation. |
commented
Apr 13, 2022
|
Huzzah! The @rust-lang/libs-api team has decided to accept this RFC. To track further discussion, subscribe to the tracking issue1. Footnotes |
Rendered
This RFC proposes adding a
provide_anymodule to the core library. The module provides a generic API for objects to provide type-based access to data. (In contrast to theanymodule which provides type-driven downcasting, the proposed module integrates downcasting into data access to provide a safer and more ergonomic API).