From 0ca39fcb2e10b81c9c3a0a8fc4df0aef6a424f26 Mon Sep 17 00:00:00 2001 From: Nick Cameron Date: Tue, 1 Mar 2022 12:07:44 +0000 Subject: [PATCH] Remove type tags and discuss mutable references Signed-off-by: Nick Cameron --- text/0000-dyno.md | 246 ++++++++++++++++++---------------------------- 1 file changed, 93 insertions(+), 153 deletions(-) diff --git a/text/0000-dyno.md b/text/0000-dyno.md index e6ad5c2ecf3..59afe72898f 100644 --- a/text/0000-dyno.md +++ b/text/0000-dyno.md @@ -6,23 +6,24 @@ # Summary [summary]: #summary -This RFC proposes adding a `provide_any` module to the core library. The module provides a generic API for objects to provide type-based access to data. (In contrast to the [`any` module](https://doc.rust-lang.org/nightly/std/any/index.html) which provides type-driven downcasting, the proposed module integrates downcasting into data access to provide a safer and more ergonomic API). +This RFC proposes extending the [`any` module](https://doc.rust-lang.org/nightly/std/any/index.html) of the core library with a generic API for objects to provide type-based access to data. (In contrast to the existing APIs which provides type-driven downcasting, the proposed extension integrates downcasting into data access to provide a safer and more ergonomic API). By using the proposed API, a trait object can offer functionality like: ```rust let s: String = object.request(); -let s = object.request_field::(); // s: &str -let x = object.request_field::(); // x: &SpecificContext +let s = object.request_field::(); // s: &str +let x = object.request_field::(); // x: &SpecificData ``` -Here, the `request` and `request_field` methods are implemented by the author of `object` using `provide_any` as a framework. +Here, the `request` and `request_field` methods are implemented by the author of `object` using the proposed API as a framework. ## Notes * A major motivation for this RFC is 'generic member access' for the `Error` trait. That was previously proposed in [RFC 2895](https://github.com/rust-lang/rfcs/pull/2895). This RFC will use `Error` as a driving example, but explicitly does not propose changes to `Error`. * A proof-of-concept implementation of this proposal (and the motivating extension to `Error`) is at [provide-any](https://github.com/nrc/provide-any). -* `provide_any` is adapted from [dyno](https://github.com/mystor/dyno/tree/min_magic). +* This work is adapted from [dyno](https://github.com/mystor/dyno/tree/min_magic). +* Previous iterations of this work included exposing the concept of type tags. These are still used in the implementation but are no longer exposed in the API. # Motivation @@ -30,7 +31,7 @@ Here, the `request` and `request_field` methods are implemented by the author of Trait objects (`Pointer`) provide strong abstraction over concrete types, often reducing a wide variety of types to just a few methods. This allows writing code which can operate over many types, using only a restricted interface. 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). -As a concrete example of the above scenario, consider the `Error` trait. It is often used as a trait objects so that all errors can be handled generically. However, classes of errors often have additional context that can be useful when handling or reporting errors. For example, an error [backtrace](https://doc.rust-lang.org/nightly/std/backtrace/struct.Backtrace.html), information about the runtime state of the program or environment, the location of the error in the source code, or help text suggestions. Adding backtrace methods to `Error` has already been implemented (currently unstable), but adding methods for all context information is impossible. +As a concrete example of the above scenario, consider the `Error` trait. It is often used as a trait object so that all errors can be handled generically. However, classes of errors often have additional context that can be useful when handling or reporting errors. For example, an error [backtrace](https://doc.rust-lang.org/nightly/std/backtrace/struct.Backtrace.html), information about the runtime state of the program or environment, the location of the error in the source code, or help text suggestions. Adding backtrace methods to `Error` has already been implemented (currently unstable), but adding methods for all context information is impossible. Using the API proposed in this RFC, a solution might look something like: @@ -44,7 +45,7 @@ struct MyError { } impl Error for MyError { - fn provide_context<'a>(&'a self, req: &mut Requisition<'a>) { + fn provide_context<'a>(&'a self, req: &mut Demand<'a>) { req.provide_ref::(&self.backtrace) .provide_ref::(&self.suggestion); } @@ -61,7 +62,7 @@ fn report_error(e: &dyn Error) { } // Help text suggestion. - // Note, we should use explicit tags or a newtype here to prevent confusing different string + // Note, we should really use a newtype here to prevent confusing different string // context information (see appendix). if let Some(suggestion) = e.get_context_ref::() { emit_suggestion(suggestion); @@ -69,21 +70,21 @@ fn report_error(e: &dyn Error) { } ``` -An alternative is to do some kind of name-driven access, for example we could add a method `fn get(name: String) -> Option<&dyn Any>` to error (or use something more strongly typed than `String` to name data). The disadvantage of this approach is that the caller must downcast the returned object and that leads to opportunities for bugs since there is an implicit connection between the name and type of objects. If that connection changes, it will not be caught at compile time, only at runtime (and probably with a panicking unwrap, since programmers will be likely to unwrap the result of downcasting, believing it to be guaranteed by `get`). Furthermore, this approach is limited by constraints on `Any`, we cannot return objects by value, return objects which include references (due to the `'static` bound on `Any`), objects which are not object safe, or dynamically sized objects (e.g., we could not return an `&str`). +An alternative is to do some kind of name-driven access, for example we could add a method `fn get(name: String) -> Option<&dyn Any>` to `Error` (or use something more strongly typed than `String` to name data). The disadvantage of this approach is that the caller must downcast the returned object and that leads to opportunities for bugs, since there is an implicit connection between the name and type of objects. If that connection changes, it will not be caught at compile time, only at runtime (and probably with a panicking unwrap, since programmers will be likely to unwrap the result of downcasting, believing it to be guaranteed by `get`). Furthermore, this approach is limited by constraints on `Any`, we cannot return objects by value, return objects which include references (due to the `'static` bound on `Any`), objects which are not object safe, or dynamically sized objects (e.g., we could not return a `&str`). -Beyond `Error`, one could imagine using the proposed API in any situation where we might add arbitrary data to a generic object. Another concrete example might be the `Context` object passed to `future::poll`. +Beyond `Error`, one could imagine using the proposed API in any situation where we might add arbitrary data to a generic object. Another concrete example might be the `Context` object passed to `future::poll`. See also a full example in the appendix. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -In this section I'll describe how the `provide_any` module is used and defined. Most of the module is not intended to be used directly. Typically there is an intermediate library which has a trait which extends `Provider` (we will use the `Error` trait in our examples) and has API which is a facade over `provide_any`'s API. `provide_any` is relatively complex in both interface and implementation. However, most users will not even be aware of its existence. Using `provide_any` in an intermediate library has some complexity, but using the intermediate library is straightforward for end users. +In this section I'll describe how the proposed API is used and defined. Typically there is an intermediate library which has a trait which extends `Provider` (we will use the `Error` trait in our examples) and has API which is a facade over `any`'s API. This proposal is relatively complex in implementation. However, most users will not even be aware of its existence. Using the `any` extensions in an intermediate library has some complexity, but using the intermediate library is straightforward for end users. -`provide_any` supports generic, type-driven access to data and a mechanism for intermediate implementers to provide such data. The key parts of `provide_any`'s interface are the `Provider` trait for objects which can provide data, and the `request_by_type_tag` function for requesting data from an object which implements `Provider`. Note that end users should not call `request_by_type_tag` directly, it is a helper function for intermediate implementers to use. +This proposal supports generic, type-driven access to data and a mechanism for intermediate implementers to provide such data. The key parts of the interface are the `Provider` trait for objects which can provide data, and the `request_*` functions for requesting data from an object which implements `Provider`. Note that end users should not call the `request_` functions directly, they are helper functions for intermediate implementers to use. -For the rest of this section we will explain the earlier example, starting from data access and work our way down the implementation to `provide_any`. Note that the fact that `Error` is in the standard library is immaterial to its use of `provide_any`. `provide_any` is a public module and can be used by any crate. We use `Error` as an example of how `provide_any` is used, we are not proposing changes to `Error` in this RFC. +For the rest of this section we will explain the earlier example, starting from data access and work our way down the implementation to the changes to `any`. Note that the fact that `Error` is in the standard library is mostly immaterial. `any` is a public module and can be used by any crate. We use `Error` as an example, we are not proposing changes to `Error` in this RFC. -A user of `Error` trait objects can call `get_context_ref` to access data by type which might be carried by an `Error` object. The function returns an `Option` and will return `None` if the requested type of data is not carried. For example, to access a backtrace (in a world where the `Error::backtrace` method has been removed): `e.get_context_ref::()` (specifying the type using the turbofish syntax may not be necessary if the type can be inferred from the context). +A user of `Error` trait objects can call `get_context_ref` to access data by type which might be carried by an `Error` object. The function returns an `Option` and will return `None` if the requested type of data is not carried. For example, to access a backtrace (in a world where the `Error::backtrace` method has been removed): `e.get_context_ref::()` (specifying the type using the turbofish syntax may not be necessary if the type can be inferred from the context, though we recommend it). Lets examine the changes to `Error` required to make this work: @@ -91,11 +92,11 @@ Lets examine the changes to `Error` required to make this work: pub mod error { pub trait Error: Debug + Provider { ... - fn provide_context<'a>(&'a self, _req: &mut Requisition<'a>) {} + fn provide_context<'a>(&'a self, _req: &mut Demand<'a>) {} } impl Provider for T { - fn provide<'a>(&'a self, req: &mut Requisition<'a>) { + fn provide<'a>(&'a self, req: &mut Demand<'a>) { self.provide_context(req); } } @@ -103,117 +104,49 @@ pub mod error { impl dyn Error + '_ { ... pub fn get_context_ref(&self) -> Option<&T> { - crate::provide_any::request_by_type_tag::<'_, tags::Ref>(self) + crate::any::request_ref(self) } } } ``` -`get_context_ref` is added as a method on `Error` trait objects (`Error` is also likely to support similar methods for values and possibly other types, but I will elide those details), it simply calls `provide_any::request_by_type_tag` (we'll discuss this function and the `tags::Ref` passed to it below). But where does the context data come from? If a concrete error type supports backtraces, then it must override the `provide_context` method when implementing `Error` (by default, the method does nothing, i.e., no data is provided, so `get_context_ref` will always returns `None`). `provide_context` is used in the blanket implementation of `Provider` for `Error` types, in other words `Provider::provide` is delegated to `Error::provide_context`. +`get_context_ref` is added as a method on `Error` trait objects (`Error` is also likely to support similar methods for values and possibly other types, but I will elide those details), it simply calls `any::request_ref` (we'll discuss this function below). But where does the context data come from? If a concrete error type supports backtraces, then it must override the `provide_context` method when implementing `Error` (by default, the method does nothing, i.e., no data is provided, so `get_context_ref` will always returns `None`). `provide_context` is used in the blanket implementation of `Provider` for `Error` types, in other words `Provider::provide` is delegated to `Error::provide_context`. Note that by adding `provide_context` with a default empty implementation and the blanket `impl` of `Provider`, these changes to `Error` are backwards compatible. However, this pattern is only possible because `Provider` and `Error` will be defined in the same crate. For third party users, users will implement `Provider::provide` directly, in the usual way. -In `provide_context`, an error type provides access to data via a `Requisition` object, e.g., `req.provide_ref::(&self.backtrace)`. The type of the reference passed to `provide_ref` is important here (and we encourage users to use explicit types with turbofish syntax even though it is not necessary, this might even be possible to enforce using a lint). When a user calls `get_context_ref`, the requested type must match the type of an argument to `provide_ref`, e.g., the type of `&self.backtrace` is `&Backtrace`, so a call to `get_context_ref::()` will return a reference to `self.backtrace`. An implementer can make multiple calls to `provide_ref` to provide multiple data with different types. +In `provide_context`, an error type provides access to data via a `Demand` object, e.g., `req.provide_ref::(&self.backtrace)`. The type of the reference passed to `provide_ref` is important here (and we encourage users to use explicit types with turbofish syntax even though it is not necessary, this might even be possible to enforce using a lint). When a user calls `get_context_ref`, the requested type must match the type of an argument to `provide_ref`, e.g., the type of `&self.backtrace` is `&Backtrace`, so a call to `get_context_ref::()` will return a reference to `self.backtrace`. An implementer can make multiple calls to `provide_ref` to provide multiple data with different types. -Note that `Requisition` has methods for providing values as well as references, and for providing more complex types. These will be covered in the next section. +Note that `Demand` has methods for providing values as well as references, and for providing more complex types. These will be covered in the next section. -The important parts of `provide_any` are +The important additions to `any` are ```rust -pub mod provide_any { - pub trait Provider { - fn provide<'a>(&'a self, req: &mut Requisition<'a>); - } - - pub fn request_by_type_tag<'a, I: TypeTag<'a>>(provider: &'a dyn Provider) -> Option { ... } +pub trait Provider { + fn provide<'a>(&'a self, req: &mut Demand<'a>); +} - pub type Requisition<'a> = ...; +pub fn request_value<'a, T: 'static>(provider: &'a dyn Provider) -> Option { ... } +pub fn request_ref<'a, T: ?Sized + 'static>(provider: &'a dyn Provider) -> Option<&'a T> { ... } - impl<'a> Requisition<'a> { - pub fn provide_ref(&mut self, value: &'a T) -> &mut Self { ... } - ... - } +pub struct Demand<'a>(...); - pub trait TypeTag<'a>: Sized + 'static { - type Type: 'a; - } - - pub mod tags { ... } +impl<'a> Demand<'a> { + pub fn provide_value T>(&mut self, fulfil: F) -> &mut Self { ... } + pub fn provide_ref(&mut self, value: &'a T) -> &mut Self { ... } } ``` -We have mostly covered how this interface is used above. The last piece is the concept of type tags, as used as a type parameter to `request_by_type_tag`. - -`core::any::TypeId` is a unique identifier for a type, but can only be used with types without lifetimes (or with only `'static` lifetimes). A type tag is an object which provides a layer of indirection to work around this limitation. For example, consider the type `&'l String`. It does not have a `TypeId` because of the `'l` lifetime. But we can use a type tag, `Ref` to represent `&'l String` and `Ref` does have a `TypeId`. - -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., ` 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. - -Intermediate libraries (such as `error` in our examples) can choose to support only common type patterns so that the user is totally unaware of type tags, or a general API using type tags, or a hybrid approach where common cases are explicitly supported for ease of use and type tags are also supported for completeness. - -Note, that in the specialised variations, the user can use the type directly, e.g., `get_context_ref::()`. But in the general case the user must provide the type tag, not the type itself, e.g., `get_by_tag::>()`. That prevents using type inference or type ascription for access, and the type tag must always be explicitly specified. - - # Reference-level explanation [reference-level-explanation]: #reference-level-explanation For details of the implementation, see the [provide-any](https://github.com/nrc/provide-any) repo which has a complete (though unpolished) implementation of this proposal, and an implementation of the extensions to `Error` used in the examples above. -I propose that `provide_any` include the following type tags: - -```rust -pub mod provide_any { - pub mod tags { - use super::TypeTag; - use core::marker::PhantomData; - - /// Type-based `TypeTag` for `&'a T` types. - pub struct Ref(PhantomData); - - impl<'a, T: ?Sized + 'static> TypeTag<'a> for Ref { - type Type = &'a T; - } - - /// Type-based `TypeTag` for `&'a mut T` types. - pub struct RefMut(PhantomData); - - impl<'a, T: ?Sized + 'static> TypeTag<'a> for RefMut { - type Type = &'a mut T; - } - - /// Type-based `TypeTag` for static `T` types. - pub struct Value(PhantomData); - - impl<'a, T: 'static> TypeTag<'a> for Value { - type Type = T; - } - - /// Tag combinator to wrap the given tag's value in an `Option` - pub struct Optional(PhantomData); - - impl<'a, I: TypeTag<'a>> TypeTag<'a> for Optional { - type Type = Option; - } - - /// Tag combinator to wrap the given tag's value in an `Result` - pub struct ResultTag(PhantomData, PhantomData); - - impl<'a, I: TypeTag<'a>, E: TypeTag<'a>> TypeTag<'a> for ResultTag { - type Type = Result; - } - } -} -``` - -For intermediate libraries, `Value` serves the common case of providing a new or temporary value (for example, computing a `String` message from multiple fields), `Ref` and `RefMut` cover the common case of providing a reference to a field of the providing object (as in `Error::context_ref` providing a reference to `self.backtrace` in the motivating examples), where the field does not contain non-static references. `Optional` is used in the implementation of `provide_any`, it is public since it seems like it could be generally useful. `ResultTag` is not strictly necessary, but is included as a complement to `Optional`. - -### Requisition +### Demand ```rust -impl<'a> Requisition<'a> { +impl<'a> Demand<'a> { /// Provide a value or other type with only static lifetimes. - pub fn provide_value(&mut self, f: F) -> &mut Self + pub fn provide_value(&mut self, fulfil: F) -> &mut Self where T: 'static, F: FnOnce() -> T, @@ -221,46 +154,20 @@ impl<'a> Requisition<'a> { /// Provide a reference, note that the referee type must be bounded by `'static`, but may be unsized. pub fn provide_ref(&mut self, value: &'a T) -> &mut Self { ... } - - /// Provide a value with the given `TypeTag`. - pub fn provide>(&mut self, value: I::Type) -> &mut Self { ... } - - /// Provide a value with the given `TypeTag`, using a closure to prevent unnecessary work. - pub fn provide_with(&mut self, f: F) -> &mut Self - where - I: TypeTag<'a>, - F: FnOnce() -> I::Type, - { ... } - } - ``` -`Requisition` is an object for provider types to provide data to be accessed. It is required because there must be somewhere for the data (or a reference to it) to exist. +`Demand` is an object for provider types to provide data to be accessed. It is required because there must be somewhere for the data (or a reference to it) to exist. `provide_value` and `provide_ref` are convenience methods for the common cases of providing a temporary value and providing a reference to a field of `self`, respectively. `provide_value` takes a function to avoid unnecessary work when querying for data of a different type; `provide_ref` does not use a function since creating a reference is typically cheap. -`provide` and `provide_with` offer full generality, but require the explicit use of type tags. An example of using `provide_with`: - -```rust -impl Error for MyError { - fn provide_context<'a>(&'a self, req: &mut Requisition<'a>) { - // Taking ownership of the string implies cloning it, so we use `provide_with` to avoid that - // operation unless it is necessary. - req.provide_with::(|| self.suggestion.to_owned()); - } -} -``` - # Drawbacks [drawbacks]: #drawbacks -There is some overlap in use cases with `Any`. It is sub-optimal for the standard library to include two modules with such similar functionality. However, I believe `provide_any` is more of a complement to `any`, rather than an alternative: `any` supports type-hiding where the concrete type is chosen by the library, whereas with `provide_any`, the library user chooses the concrete type. +There is some overlap in use cases with `Any`. It is sub-optimal for the standard library to include two systems with such similar functionality. However, I believe the new functionality is a complement to `any`, rather than an alternative: `any` supports type-hiding where the concrete type is chosen by the library, whereas with `Provider`, the library user chooses the concrete type. -`provide_any` could be a stand-alone crate, however, a key motivation for `provide_any` is use in `Error` and that is only possible if `provide_any` is part of the standard library. - -`provide_any` is fairly complex, however, this is mitigated by restricting the exposed complexity to intermediate implementers or to users with advanced use cases. For many Rust programmers, they won't even know `provide_any` exists, but will reap the benefits via more powerful error handling, etc. +This proposal is fairly complex, however, this is mitigated by restricting the exposed complexity to intermediate implementers or to users with advanced use cases. For many Rust programmers, they won't even know the implementation exists, but will reap the benefits via more powerful error handling, etc. # Rationale and alternatives @@ -270,15 +177,13 @@ There are are few ways to do without the proposed feature: in some situations it Each of these approaches has significant downsides: adding methods to traits leads to a confusing API and is impractical in many situations (including for `Error`). Value-driven access only works with types which implement `Any`, that excludes types with lifetimes and unsized types; furthermore it requires the caller to downcast the result which is error-prone and fragile. Speculative downcasting violates the encapsulation of trait objects and is only possible if all implementers are known (again, not possible with `Error`). -`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`), or the contents of `provide_any` could be added to `any`. +The proposed API could live in its own crate, rather than in libcore. However, this would not be useful for `Error` (or other standard library types). -There are numerous ways to tweak the API of a module like `provide_any`. The `dyno` and `object-provider` crates provide two such examples. There are many others, for example providing more patterns of types without requiring tags, not providing any common type patterns (i.e., always requiring tags), not exposing tags at all, etc. +As in earlier drafts of this RFC, the proposed API could be in its own module (`provide_any`) rather than be part of `any`, either as a sibling or a child of `any`. -One hazard that must be avoided with such tweaks is that the API must distinguish between reference types with a `'static` lifetime and value types (with no lifetimes), either by using type tags or by having separate mechanisms for handling references and values. If this is done improperly, the API could be unsound. As an example, consider an API which lets an object provide a reference with type `&'a str` (where `'a` is the lifetime of some local scope), then a caller requests an object of type `&'static str` using an API designed for values (possible because that type satisfies the `'static` bound). Without some care, it is possible for the two types to be confused (because the lifetime is not reflected in the type tag or `TypeId`) and for the `&'a str` to be returned with the wrong lifetime. I believe this is not possible in the current proposal, but was possible in an earlier, more ergonomic iteration. +There are numerous ways to tweak the proposed API. The `dyno` and `object-provider` crates provide two such examples. -A slightly different approach is to remove the convenience methods and always require the use of type tags. I believe this is sub-optimal since it requires users to learn a new concept (type tags) and makes using the methods less ergonomic. However, it does have advantages: code is less prone to silent inference errors since the type tag must always be explicitly specified, and the API is smaller. +We could expose type tags (as used in the implementation of these APIs) to the user. However, whether to do so, and if so exactly how type tags should work (even the key abstractions) are open questions (see below for more discussion). Exposing type tags to the user makes for a much more flexible API (any type can be used if the user can write their own tags), but it requires the user to understand a somewhat subtle and complex abstraction. The API as currently presented is simpler. # Prior art [prior-art]: #prior-art @@ -286,7 +191,7 @@ A slightly different approach is to remove the convenience methods and always re Operations involving runtime types are intrinsically tied to the specifics of a language, its runtime, and type system. Therefore, there is not much prior art from other languages. -A closely related feature from other languages is reflection. However, reflective field access is usually name-driven rather than type-driven. Due to Rust's architecture, general reflection is impossible. +A closely related feature from other languages is reflection. However, reflective field access is usually name-driven rather than type-driven. Due to Rust's architecture, general reflection is 'impossible'. In Rust, there are several approaches to the problem. This proposal is adapted from [dyno](https://github.com/mystor/dyno/); [object provider](https://github.com/mystor/object-provider/) is a similar crate from the same author. @@ -298,39 +203,74 @@ Some Rust error libraries provide roughly similar functionality. For example, [A Can we improve on the proposed API? Although we have iterated on the design extensively, there might be room for improvement either by general simplification, or making the most general cases using type tags more ergonomic. -As with any relatively abstract library, naming the concepts in `provide_any` is difficult. Are there better names? In particular, 'tag' and 'requisition' are very generic terms which could indicate their purpose better. +As with any relatively abstract library, naming the concepts here is difficult. Are there better names? In particular, 'demand' (formerly 'requisition') is very generic. Extending the API of `Error` is a primary motivation for this RFC, but those extensions are only sketched in the examples and implementation. What exactly should `Error`'s API look like? -It was suggested by @Plecra in the [comments](https://github.com/rust-lang/rfcs/pull/2895#issuecomment-735713486) of RFC 2895, that this mechanism could be used for providing data from a future's `Context` object. That is a more demanding application since it is likely to require `&mut` references, objects with complex lifetimes, and possibly even closures to be returned. That has motivated seeking a general API for `provide_any`, rather than only supporting `'static` lifetimes. +It was suggested by @Plecra in the [comments](https://github.com/rust-lang/rfcs/pull/2895#issuecomment-735713486) of RFC 2895, that this mechanism could be used for providing data from a future's `Context` object. That is a more demanding application since it is likely to require `&mut` references, objects with complex lifetimes, and possibly even closures to be returned. That had motivated seeking a general API, rather than only supporting references and values. -Recommendations for how to use the API, in particular when to provide or request a value using a plain type, type tag, or special purpose newtype. See the appendix for discussion and examples. +The implementation of the proposed API uses type tags. These are similar to the `Any` trait in that they allow up- and down-casting of types, however, they are fundamentally different in that the abstract trait is not implemented by the concrete type, but rather there is a separate type hierarchy of tags which are a representation of the type. This allows lifetimes to be accurately reflected in the abstract types, which is crucial for the sound implementation of this API. Exactly how these tags should be represented, however, is unresolved. There is one implementation in the [provide-any](https://github.com/nrc/provide-any) repo, this is an adaption of [dyno](https://github.com/mystor/dyno/). @Plecra has proposed an [alternative encoding](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=262e0d299bdd4b8e38ac25faaa0b8887). There are probably others. Perhaps adding language features will allow still more. I expect this part of the implementation can be tweaked over time. The proposed API is designed to keep type tags fully encapsulated so that they can evolve without backwards compatibility risk. # Future possibilities [future-possibilities]: #future-possibilities -A possible extension to this work is full downcasting using type tags - a proper alternative to `Any`. Such downcasting is available in dyno and is an implementation detail of the proof of concept implementation for this RFC. It is not part of `provide_any`'s interface in order to minimise the proposal and avoid overlap with `Any`, however, I don't see any technical issues with exposing such functionality in the future if there is demand. +Type tags (discussed above) are part of the implementation of this proposal. In the future, they could be exposed to the user to make a more flexible, general API. Even with this more flexible API, we would still want to keep the existing API for convenience. -# Appendix: using tags or newtypes +The proposal only supports handling value types (or other types with only `'static` lifetimes) and reference types. We could also support mutable references fairly easily. Note, however, that this requires more methods than one might expect since providing mutable references requires a mutable reference to the provider, but for non-mutable references requiring a mutable provider is overly restrictive. Furthermore, providing multiple mutable references to different types requires extending the API of `Demand` in a non-trivial way (this is done in [provide-any](https://github.com/nrc/provide-any) to demonstrate feasibility, thanks to @Plecra for the implementation). Therefore, I have not included handling of mutable references in this proposal, but we could easily add such support in the future. -One issue with using type-based provision is that an object might reasonably provide many different values with the same type. If the object intends to provide multiple values with the same type, then it must distinguish them. Even if it doesn't, the user might request a value expecting one thing and get another with the same type, leading to logic errors (e.g., requesting a string from `MyError` expecting an error code and getting a suggestion message). -There are two possible solutions (both of these use the API as proposed, neither requires extensions): wrap values in value-specific newtypes, or use value-specific type tags. +# Appendix 1: using newtypes + +One issue with using type-based provision is that an object might reasonably provide many different values with the same type. If the object intends to provide multiple values with the same type, then it must distinguish them. Even if it doesn't, the user might request a value expecting one thing and get another with the same type, leading to logic errors (e.g., requesting a string from `MyError` expecting an error code and getting a suggestion message). -I'll explain these options in the context of the `MyError` example. +To address this, users should use specific newtypes to wrap more generic types. E.g., the `MyError` author could provide a `Suggestion` newtype: `pub struct Suggestion(pub String)`. A user would use `get_context::()` (or otherwise specify the type, e.g., `let s: Suggestion = e.get_context().unwrap();`) and would receive a `Suggestion` which they would then unpack to retrieve the string value. -Using newtypes, the `MyError` author would provide a `Suggestion` newtype: `pub struct Suggestion(pub String)`. A user would use `get_context::()` (or otherwise specify the type, e.g., `let s: Suggestion = e.get_context().unwrap();`) and would receive a `Suggestion` which they would then have to unpack to retrieve the string value. This approach only works for owned `String`s: newtypes cannot wrap unsized values, and keeping a reference in the newtype would require using an explicit tag, making the newtype option strictly worse than the type tag option. +# Appendix 2: plugin example -Using type tags, the `MyError` author would provide a `SuggestionTag` type tag: +This appendix gives another example of the proposed API in action, this time for an intermediate trait which is not part of the standard library. The example is a systems with a plugin architecture where plugins extend `Provider` so that plugin authors can access arbitrary data in their plugins without having to downcast the plugins. In the example I show the plugin definition and a single plugin (`RulesFilter`), I assume these are in different crates. `RulesFilter` uses the provider API to give access to a statistics summary (produced on demand) and to give access to a borrowed slice of its rules. ```rust -pub struct SuggestionTag; +// Declared in an upstream crate. +pub trait Plugin: Provider { ... } + +impl dyn Plugin { + pub fn get_plugin_data(&self) -> Option { + any::request_value(self) + } -impl<'a> TypeTag<'a> for SuggestionTag { - type Type = &'a str; + pub fn borrow_plugin_data(&self) -> Option<&T> { + any::request_ref(self) + } } -``` -The user would use `get_context_by_type_tag::` to retrieve the suggestion. This approach supports references, so the suggestion message does not need to be cloned, and returns the string directly to the user without them having to unpack a newtype. However, it does require the user to understand the type tag concept. +// Plugin definition for RulesFilter (downstream) +struct RulesFilter { + rules: Vec, +} + +impl Plugin for RulesFilter { ... } + +struct Statistics(String); + +impl Provider for RulesFilter { + fn provide<'a>(&'a self, demand: &mut Demand<'a>) { + demand + .provide_value(|| Statistics(format!(...))) + .provide_ref::<[Rule]>(&self.rules); + } +} + +fn log_plugin_stats(plugin: &dyn Plugin) { + if let Some(Statistics(s)) = plugin.get_plugin_data() { + log(plugin.id(), s); + } +} -Note that the type tags option relies on their being a potential many-to-one relationship between type tags and types. +fn log_active_rules(f: &mut dyn Plugin) { + if let Some(rules) = plugin.borrow_plugin_data::<[Rule]>() { + for r in rules { + log(plugin.id(), r); + } + } +} +```