Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Generic member access for dyn Error trait objects #2895
base: master
Are you sure you want to change the base?
RFC: Generic member access for dyn Error trait objects #2895
Changes from 5 commits
39fc23e
63d0539
6d9a212
c1fd1f6
f344bd9
9726cfe
9dd4113
49fc1d0
f2073ac
1fc15e9
460c523
c8b80ed
f16045d
e12ba35
9581f78
6143f6d
1bb8ca7
e82ac82
53431f5
6451b1c
f74f64f
75ef121
ac0f94e
8d55678
7f87544
bcb4823
2ea013d
248e4ca
ef2e47e
d055bbb
ac79814
41b589b
defeafe
48adce6
0d441ac
7b98760
407ce78
84c8bf7
fb02f91
61be66b
File filter
Filter by extension
Conversations
Jump to
There are no files selected for viewing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be better to cite the mechanism this uses in addition to/instead of the name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that might be more detail than is necessary for the RFC, I've merged it with the "alternatives to std::backtrace" bullet and it has a docs.rs link
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a strong and attractive claim but to me needs expansion/defense. i.e. are there really no other reasons to have it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I can think of, the only other purpose it has is downcasting, so if you wanted a type that could contain an open set of errors where the only thing you do with them is downcast them back to concrete types you'd just use
dyn Any
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd start the walkthrough with the motivating output, much better headline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean move this to the top of the guide level explanation? or to motivation / summary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these two lines don't really do anything, I assume they're stand-ins for "extracting Location types from errors gathered via #[track_caller] or similar" mentioned previously? Is there a link to a proposal for how that might be done? Maybe it's already possible and I just haven't been watching
track_caller
close enough?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct. It is already possible to do this with
track_caller
, so we could update this to includetrack_caller
. I don't recall if there was a specific reason I avoided using track_caller here but I don't mind updating this example.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A potential additional drawback is that requesting types dynamically like this can lead to surprising changes in runtime behavior when changing versions of (transitive) dependencies.
Given:
foo
with versions 1.0.0 and 2.0.0, each of which provides a typeFoo
.bar
which depends on version 1.0.0 offoo
and whose errors provideFoo
.baz
which dependsbar
and on version 2.0.0 offoo
and tries to requestFoo
frombar
's errors.Then a situation is created in which
baz
will fail to requestFoo
frombar
's error because the requestedFoo
and providedFoo
are actually distinct types because the transitive dependencyfoo
's versions don't match up.This could even happen in a semver-compatible release of
bar
andbaz
because technically the API is not broken: the type can still be requested, but it will simply begin returningNone
instead ofSome(_)
. This would be even worse iffoo
,bar,
andbaz
are all library crates depended on by an application crate and the semver-incompatible versions offoo
are introduced by a semver-compatible change inbar
orbaz
which would get pulled in by runningcargo update
on the application crate. The application crate has no recourse in this situation aside from waiting forbar
andbaz
to agree on a semver-compatible version offoo
again.(I've been playing with an alternate design based on an associated type rather than generic member access which causes this situation to be a compile time error instead of a runtime behavior change, but that design requires traits with lifetime GATs to be dyn safe to be actually good, and associated type defaults for it to maybe be possible to retrofit that design onto the standard library's existing error trait.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree this is a footgun I'd prefer to avoid but as far as I can tell this is not a problem being uniquely introduced by generic member access. This is a problem with type erasure in general and the same situation could be encountered by having foo return
Foo
as aBox<dyn Any>
in any of it's public API's. Ifbaz
tries to downcast using it's own path tofoo::Foo
it will run into the same type ID mismatch.I avoid this in my crates currently by re-exporting types from crates that are part of my public API (https://docs.rs/color-eyre/latest/color_eyre/#reexports). Applying this to your example you could avoid this footgun if you downcast/request
Foo
viabar::foo::Foo
which would correctly resolve to whatever version it is using. This of course gets a little silly when you want to be able to handle all the versions ofFoo
, because then presumably you have to try to requestFoo
once for each path through which you could possibly reach it, which is definitely a subpar user experience, so not necessarily a great solution.That said, I was never able to figure out how to implement generic member access w/o type erasure and runtime checks. I'm curious to understand more about your potential alternative design, because I would definitely love to see a better solution but I don't see how it is possible given the constraints of the error trait and the goals of generic member access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True.
However, it seems to me that this kind of thing is relatively rare currently, both in the standard library and ecosystem crates. The only standard library exception I can think of is
std::io::Error
, for which I found (part of?) the rationale for in rust-lang/rust#24793 (comment), and I haven't personally encountered any ecosystem crates that make use of this and tell users in documentation that they can/should downcast the inner error to some concrete type.So, I think making type erasure a central part of the error handling API of the standard library would probably be the main place this kind of problem would come up. I'm not sure the existing occasional cases of downcasting in public APIs can be used to justify a standard library API built entirely on downcasting.
While true, I imagine another place this might become a problem is in a library whose purpose is to use generic member access to generate pretty error reports. This hypothetical library might hypothetically pull in the
backtrace
crate for handling backtraces. If any other crate in the dependency tree has errors that provide a backtrace from a different version of thebacktrace
crate, the hypothetical library will not be able to extract them. I don't think this can be solved with re-exports, because that would force the library to have a re-export of every crate that has errors that provide abacktrace
crate backtrace.Your next point about requesting the type through each path (including crate version) does provide a solution there, but also as you point out, would not be the most fun to maintain.
I can't promise that this particular idea is any better, but for what it's worth, it goes like this:
As written, this requires some language features that aren't implemented yet:
core::error::Error
, but luckily not required for defining a new error trait in a new crate.dyn
-safety for traits with lifetime GATs. In my current implementation, I've simply dropped the lifetime and am just returning an owned value, which has some overhead but at least allows experimenting with the rest of the design.Another part of this design is a
map
-like function to change the type ofDetails
when composing errors that have different concrete types.To approximate generic member access to make it possible to implement a hypothetical library for pretty-printing errors like described above, I imagine a crate could define some traits like
trait GetBacktrace { fn get_backtrace(&self) -> Option<&Backtrace>; }
(or one trait with many methods, I suppose) and then adding these traits as bounds onError::Details
on the pretty-printing functions. The author of a binary crate could then implement these traits for theirError::Details
type and use the library's pretty-printing code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't that still have a similar problem? To adapt your example from before:
Details
type that implements FooError::Details
to implement Foo (from version 2.0.0 of foo)baz still can't get the the
Foo
details from errors from bar.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct, but the difference is how it can't get the details. With the generic member access design, this would fail at run time: the generic member access functions would return
None
. With the associated type design, this would fail at compile time: a compilation error would be emitted about the trait bound being unsatisfied.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can currently have a
Box<dyn Error + 'static>
(or reference, or some other lifetime). Associated types and dyn-safety for traits with lifetime GATs isn't enough. If you had aDetails
type that isn't equal to the default, your error won't be compatible with anything that types a dynamic error, without some other kind of mechanism. And there may be some benefit to a compile time check. But that also means you lose use cases where you might want to extract data dynamically without knowing if it exists at runtime.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess a really important question is how many notable occurrences of
Box<dyn Error>
(and similar) are there in public APIs? The only one I can think of off the top of my head isstd::io::Error::{other, into_inner}
. In such a case, I believe themap
function I alluded to before should be sufficient. To extract the originalDetails
, you'd have to downcast to the concrete type first, which I think is not really a regression because that's howstd::io::Error
is currently intended to work. For internal-only APIs, the type could simply be changed to specify the desiredDetails
.I suspect these cases could be covered for
Error
-processing functions by adding bounds onDetails
of traits that contain accessor functions that returnOption<T>
so thatError
implementors have the, uh, option of returningNone
. Are there any situations where that would be insufficient?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I think returning a
Result<(), Box<dyn Error + 'static>
from main is a somewhat common pattern. And while you could maybe replace that withResult<(), Box<dyn Error<Details=SomeType> + 'static>
, you would still need the same Details type for all errors, which gets messy if your errors come from different sources.And you have libraries like anyhow that make heavy use of
dyn core::error:Error
, such as inroot_cause()
andchain()
andfrom_boxed()
in anyhow.But that means you have to know what the concrete type is. And
Details
would need to implement a trait that has support for downcasting. And how do you handle combining multiple detail types into the a single error. Do you try casting to a struct that has the exact combination of details you expect? Do you use a collection of Details objects, and then try downcasting each one of those to what you expect?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return type of
main
is not a public API, and the requirements of theTermination
trait allow you to choose other types for theErr
variant, so this would be easy to change to specify a details type. (Specifically, the requirement on theErr
variant is justDebug
.)I don't think this really counts towards what I was asking either. For
anyhow
(and similar crates) to be relevant here we'd need to know how often other library crates haveanyhow::Error
in their public interface.Worth noting that this is not a regression from the status quo.
You would use the
map
function to compose detail types. You could implement your details type to do any of those things if you wanted, but I think a simpler approach would be toimpl From<OtherDetails> for MyDetails
or so.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this misplaced?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, but I wasn't really sure how to put a code snippet in a bullet list