Skip to content
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

Add downcasting to std::error::Error #24793

Merged
merged 1 commit into from May 1, 2015
Merged

Conversation

aturon
Copy link
Member

@aturon aturon commented Apr 24, 2015

This commit brings the Error trait in line with the Error interoperation
RFC
by adding downcasting,
which has long been intended. This change means that for any Error
trait objects that are 'static, you can downcast to concrete error
types.

To make this work, it is necessary for Error to inherit from
Reflect (which is currently used to mark concrete types as "permitted
for reflection, aka downcasting"). This is a breaking change: it means
that impls like

impl<T> Error for MyErrorType<T> { ... }

must change to

impl<T: Reflect> Error for MyErrorType<T> { ... }

This commit furthermore marks Reflect as stable, since we are already
essentially committed to it via Any. Note that in the future, if we
determine that the parametricity aspects of Reflect are not needed, we
can deprecate the trait and provide a blanket implementation for it
for all types (rather than by using OIBIT), which would allow all
mentions of Reflect to be dropped over time. So there is not a strong
commitment here.

[breaking-change]

r? @alexcrichton

@alexcrichton
Copy link
Member

@bors: r+ e912b2a p=1

Nominating for a cherry-pick into beta as well

@bors
Copy link
Contributor

bors commented Apr 25, 2015

⌛ Testing commit e912b2a with merge 0ffa676...

@bors
Copy link
Contributor

bors commented Apr 25, 2015

💔 Test failed - auto-linux-64-opt

@reem
Copy link
Contributor

reem commented Apr 25, 2015

@aturon is it really necessary to mark Reflect stable here? One could just as easily use Any there.

EDIT: Note that the deprecation path you describe wouldn't be forwards compatible, since downstream users could add negative implementations of Reflect to their own types if we expose it.

#[unstable(feature = "core",
reason = "unclear whether to commit to this public implementation detail")]
fn type_id(&self) -> TypeId where Self: 'static {
TypeId::of::<&'static Self>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not TypeId::of::<Self>? TypeId supports T: ?Sized

@reem
Copy link
Contributor

reem commented Apr 25, 2015

Should we also add by-value downcasting from Box<Error> to Box<T>?

@aturon
Copy link
Member Author

aturon commented Apr 25, 2015

@reem The need for Reflect stabilization has to do with the fact that Any requires 'static, which is a constraint we do not wish to impose on Error. (It makes good sense for Any, since the trait is only usable with 'static values anyway, but Error of course has more uses than downcasting.) So the two are not equivalent.

re: forward compatibility of the deprecation path -- good point. This will require blanket impls and OIBIT to coexist. But that seems plausible (cc @nikomatsakis).

I suppose the main alternative to this change, which we discussed on IRC, would be to wait until upcasting it working and then ask people to use e.g. Box<Error + Any> when they want downcasting, rather than baking it in to the error type. As we discussed, this increases flexibility, but makes it much easier to lock APIs out of the ability to downcast, and then be stuck. Since downcasting is a fairly central part of Error, I'm still inclined toward this change.

FYI: I also updated with your other suggestions. Thanks!

@alexcrichton @nikomatsakis thoughts?

#[doc(hidden)]
#[unstable(feature = "core",
reason = "unclear whether to commit to this public implementation detail")]
fn type_id(&self) -> TypeId where Self: 'static {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you move the Reflect bound to a method-level where clause here, then we can use exclusively Any and avoid stabilizing Reflect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, actually this will prevent downcasting with Box<Error>, rather than Box<Error + Reflect>, which iirc isn't even allowed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drat, it was an interesting idea!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this some more, and we can get away without stabilizing Reflect with just a bit less flexibility - instead of implementing Error for T: Reflect in e.g. TryLockError, just implement for T: Any. It's slightly less flexible, but it's backwards compatible to generalize the impls when/if we do stabilize Reflect directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reem Ah yes, I think at some point in the iterations on this topic I confused myself into thinking this would hurt more than it does: you can still have non-'static generic error types, they will just fail to be Error for the time being. That seems plausible, and I agree that not stabilizing Reflect is a good idea.

@pnkfelix
Copy link
Member

triage: beta-nominated

Because @alexcrichton said so above. (Not r+'ing though since @aturon is awaiting feedback )

@rust-highfive rust-highfive added the beta-nominated Nominated for backporting to the compiler in the beta channel. label Apr 26, 2015
@glaebhoerl
Copy link
Contributor

@aturon Back in the RFC thread you appeared to agree with me that dynamically typed error-downcasting is an "anti-pattern" (one should use enums). Has something changed?

@alexcrichton
Copy link
Member

@aturon I think after some more discussion we agreed on not stabilizing Reflect, leaving it as a super trait, and just using Any as a bound everywhere, right? I would personally prefer to avoid Error + Any in trait objects as if it's a pillar of Error that you can do downcasting then it'd be unfortunate to forget the Any.


@glaebhoerl

The addition of downcasting here is one of the pillars of io::Error as it's the bread-and-butter error type for all of I/O (e.g. it's on io::{Write, Read} etc). As it is such a central error type it needs the ability to carry custom payloads to be applicable for everyone using the I/O traits. As a result in this case we're adding downcasting.

@reem
Copy link
Contributor

reem commented Apr 27, 2015

There's also the matter that you can't actually do Error + Any. Any is kind
of useless except as a bound (because of Reflect) or a trait to use when
you want only downcasting (from any type).
On Mon, Apr 27, 2015 at 7:35 PM Alex Crichton notifications@github.com
wrote:

@aturon https://github.com/aturon I think after some more discussion we
agreed on not stabilizing Reflect, leaving it as a super trait, and just
using Any as a bound everywhere, right? I would personally prefer to
avoid Error + Any in trait objects as if it's a pillar of Error that you

can do downcasting then it'd be unfortunate to forget the Any.

@glaebhoerl https://github.com/glaebhoerl

The addition of downcasting here is one of the pillars of io::Error as
it's the bread-and-butter error type for all of I/O (e.g. it's on io::{Write,
Read} etc). As it is such a central error type it needs the ability to
carry custom payloads to be applicable for everyone using the I/O
traits. As a result in this case we're adding downcasting.


Reply to this email directly or view it on GitHub
#24793 (comment).

@aturon
Copy link
Member Author

aturon commented Apr 28, 2015

@glaebhoerl Continuing along the lines of what @alexcrichton said: it's still the case that enums are the recommended, conventional way to provide concrete error types, and should be preferred to downcasting when practical.

That said, there are places where downcasting is appropriate. One important example that @alexcrichton is the io::Error type. During IO reform, we initially proposed for various traits like Read/Write have an associated error type, but in practice we found this to come at a massive cost in ergonomics, and instead reverted to a single concrete error type. But it's still sometimes desirable to pass a custom error through the traits. To enable this, the error type provides a "custom" variant that is a boxed Error, and is intended for downcasting.

Now, I've long maintained that such use cases are important, but why build downcasting into the Error trait itself? In general, we often have to make tough decisions about traits that are primarily used for trait objects (rather than generic bounds): that's a one-way abstraction, so if one API gives you a Box<Error> but downcasting was separate (say, you'd have to have Box<ReflectError>), then you're stuck -- you would not be able to downcast, and it would be a breaking change to alter the type. Similar problems arise with marker traits (Box<Error + Send>). So in these cases, we have to think carefully about the anticipated uses of trait objects, and try to make a good decision about the defaults, to decrease the likelihood of painting your APIs into a corner.

With all of that said, one of the primary use cases of Error for trait objects is indeed downcasting. (The other being extracting textual messages/cause chains.) Baking it in this way ensures that all uses of the Error trait are ready to handle this use case. This doesn't impose a burden on implementors of the trait, but it might indeed suggest that downcasting is more "blessed" than we mean it to be. We'll have to counter this with strong conventions documentation.

@pnkfelix
Copy link
Member

triage: beta-accepted

@pnkfelix pnkfelix added the beta-accepted Accepted for backporting to the compiler in the beta channel. label Apr 30, 2015
@glaebhoerl
Copy link
Contributor

@aturon Thanks for explaining! An associated error type was indeed the alternative solution I had in mind, but if that was tried and proved untenable, then I guess that's that. I'm mostly concerned because dynamically downcasting exception types happens to be the idiomatic error handling solution in a large number of very popular languages, and it would be very easy to see that become the done thing in Rust as well if we're not watchful. I guess we'll have to make sure the documentation is clear about this. :)

@aturon aturon force-pushed the io-error-any branch 2 times, most recently from a010bf6 to 6436123 Compare April 30, 2015 22:01
@aturon
Copy link
Member Author

aturon commented Apr 30, 2015

@alexcrichton @reem Sorry for the delay here, been in intense meeting mode this week.

I've pushed an update which leaves Reflect unstable, and customizes the error message to suggest bounding by Any. re-r?

@@ -222,7 +220,7 @@ impl TypeId {
/// Returns the `TypeId` of the type this generic function has been
/// instantiated with
#[stable(feature = "rust1", since = "1.0.0")]
pub fn of<T: ?Sized + Any>() -> TypeId {
pub fn of<T: ?Sized + Reflect + 'static>() -> TypeId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to just use Any here, the docs will be clearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to use Reflect here to allow Error to implement downcasting without inheriting from Any.

@alexcrichton
Copy link
Member

r=me with a few final nits, thanks @aturon!


#[unstable(feature = "error_downcast", reason = "recently added")]
/// An extension trait for downcasting from boxed Errors
pub trait BoxErrorExt {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we have a method on Error which takes self: Box?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we can indeed! I sometimes forget we still have that special treatment of Box.

This commit brings the `Error` trait in line with the [Error interoperation
RFC](rust-lang/rfcs#201) by adding downcasting,
which has long been intended. This change means that for any `Error`
trait objects that are `'static`, you can downcast to concrete error
types.

To make this work, it is necessary for `Error` to inherit from
`Reflect` (which is currently used to mark concrete types as "permitted
for reflection, aka downcasting"). This is a breaking change: it means
that impls like

```rust
impl<T> Error for MyErrorType<T> { ... }
```

must change to something like

```rust
impl<T: Reflect> Error for MyErrorType<T> { ... }
```

except that `Reflect` is currently unstable (and should remain so for
the time being). For now, code can instead bound by `Any`:

```rust
impl<T: Any> Error for MyErrorType<T> { ... }
```

which *is* stable and has `Reflect` as a super trait. The downside is
that this imposes a `'static` constraint, but that only
constrains *when* `Error` is implemented -- it does not actually
constrain the types that can implement `Error`.

[breaking-change]
@aturon
Copy link
Member Author

aturon commented May 1, 2015

@reem: FYI, I've incorporated most of your suggestions, except for those that are not possible.

@aturon
Copy link
Member Author

aturon commented May 1, 2015

@bors: r=alexcrichton p=10

Prioritizing due to breakage.

@bors
Copy link
Contributor

bors commented May 1, 2015

📌 Commit a576262 has been approved by alexcrichton

@bors
Copy link
Contributor

bors commented May 1, 2015

⌛ Testing commit a576262 with merge 5c710b5...

bors added a commit that referenced this pull request May 1, 2015
This commit brings the `Error` trait in line with the [Error interoperation
RFC](rust-lang/rfcs#201) by adding downcasting,
which has long been intended. This change means that for any `Error`
trait objects that are `'static`, you can downcast to concrete error
types.

To make this work, it is necessary for `Error` to inherit from
`Reflect` (which is currently used to mark concrete types as "permitted
for reflection, aka downcasting"). This is a breaking change: it means
that impls like

```rust
impl<T> Error for MyErrorType<T> { ... }
```

must change to

```rust
impl<T: Reflect> Error for MyErrorType<T> { ... }
```

This commit furthermore marks `Reflect` as stable, since we are already
essentially committed to it via `Any`. Note that in the future, if we
determine that the parametricity aspects of `Reflect` are not needed, we
can deprecate the trait and provide a blanket implementation for it
for *all* types (rather than by using OIBIT), which would allow all
mentions of `Reflect` to be dropped over time. So there is not a strong
commitment here.

[breaking-change]

r? @alexcrichton
@bors bors merged commit a576262 into rust-lang:master May 1, 2015
@alexcrichton alexcrichton removed the beta-nominated Nominated for backporting to the compiler in the beta channel. label May 5, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
beta-accepted Accepted for backporting to the compiler in the beta channel.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants