-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 for adding Sync to io::Error #1057
Conversation
This seems to be a sort of general abstraction leakage when using trait objects - there's no way to add bounds to internal trait objects without changing the definition of the type. This is particularly problematic with marker traits: for instance, I can easily see this coming up with |
I'm a little hesitant about the motivation here as there seem to be two separate points that are seem to be mixed up a bit:
The second of these motivations is solved with an I would personally prefer to see |
@alexcrichton The motivation there is "I want to put an I don't consider "put it in an Also FWIW, I think
Errors shouldn't have interior mutability (whether thread-safe or not). Calling the various The only real justification for not having |
Hm, I'm personally still finding it difficult to grasp why we want
I feel like the generalization in this case is taking it one step too far. The base rationale for this RFC seemed to be a desire to clone an
As you point out,
To me it's also API/implementation detail pollution. We require @reem is right in that this is an unfortunate abstraction leakage that we have to deal with this at all, but this is the consequence of using a trait object! |
That's my original motivation, yes. But it's not the only reason to want this behavior. Do you not like the future / promise example? A future is a write-once value. If I were to make a Future type, I'd want to have an API that includes something like enum FutureResult<T,E> {
Pending,
Fulfilled(T),
Rejected(E)
}
impl Future<T,E> {
fn get(&self) -> FutureResult<&T,&E>;
} Since it's write-once, once the value has been written, there's no reason to need any kind of synchronization (internally it would probably use an atomic bool, and once it reads that the bool is More specifically, requiring a But the only way to use this type with
If it's expected for
That's not the argument I made. My argument was that When using generics, it makes sense to avoid as many bounds as possible, because each bound further constrains what types can be used with it. But the situation is a bit different with trait objects. By not specifying the If |
Oh no sorry, I just didn't read it closely enough! This definitely seems like a plausible example.
Right, but in my mind it's a slippery slope to adding all these trait bounds. I suspect that plausible examples can be contrived for both requiring a marker trait bound as well as requesting that a marker trait bound doesn't exist. For example if I can make an example that means that trait bound Largely what I'm getting at is that I just want to make sure that requiring this bound is well motivated beyond "my previous code is no longer compiling", which doesn't sound to be the case. You've proposed some legitimate use cases for requiring
True, but I think this is a decision that has to be made either way. Types with generics leave the decision up to instantiators, but trait objects either opt-in immediately or opt-out, both of which have pros and cons. |
What would be an ergonomic solution to the general problem (abstraction leakage)? Is there one? Are there examples of how other languages solve similar problems? |
One general solution is to enable dynamic (conditional) upcasting, e.g. from |
That doesn't seem so nice to me; it solves the problem only at runtime cost. Actually, I just realized we already sort of support a solution to this by parameterizing over the constraint: use std::error;
use std::fmt;
use std::thread;
#[derive(Debug)]
struct Foo;
impl fmt::Display for Foo {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
Ok(())
}
}
impl error::Error for Foo {
fn description(&self) -> &str { "" }
}
struct Error<C: ?Sized> where C: error::Error {
inner: Box<C>
}
type CurrentVersionOfIoError = Error<error::Error + Send>;
fn main() {
let foo = Error {
inner: Box::new(Foo) as Box<error::Error + Send + Sync>
};
thread::spawn(move || {
let x = foo.inner.description();
});
} Seems awkward to switch to this though, but at least there is a potential solution for future libraries (I honestly had no idea this worked). |
@pythonesque that's super interesting, but doesn't work for The idea of trait objects is generally to move type-related activities from compile time to run time, so it seems acceptable to just add more dynamic capabilities to solve this problem. |
@pythonesque That's not doable because the whole point of |
@kballard It does erase the type of the custom error. The type of @reem The point of trait objects is type erasure, sure, but the only where it has to leak into the runtime. In cases like this, we would still know everything we needed at compile time. I do see your point though, that this is just moving the problem of what bounds |
@pythonesque Adding a generic parameter to the type and putting the bounds in there doesn't change anything. You've already recognized that it has the same forwards compatibility hazard (and no, associated types don't solve this), and you're still even using a trait object. In fact, your |
@pythonesque associated types do solve this in concrete cases but make generics and trait objects using these traits rather problematic. |
@kballard Why don't associated types solve the forward-compatibility problem? Wouldn't that allow each concrete implementation of these traits to decide which constraint to use? And can't the |
@pythonesque I'm not sure exactly what you're proposing when you say to use associated types. As long as In any case, this is getting rather off-track from the point of this RFC. |
@pythonesque fwiw the original Read and Write traits in new io used associated types for the errors, but it causes a large number of problems (e.g. can't copy from a Read with one error to a Write with a different error). |
Ah, fair enough :) Then yeah, I'm in favor of this RFC. |
I'm in favor of this RFC. In general, the issue of which marker traits to include when using trait objects is a tricky one, but I think a case like this (where a trait object is being stored in an important, concrete type) should err in the direction of including "standard" markers. (Over time, we're going to need to develop a more robust policy in std; at the moment, we just don't use trait objects in public APIs very often.) |
Thanks again for the RFC @kballard! The feedback here is pretty positive and at this time we've decided to merge this! (my concerns have been addressed) |
Rendered
A PR for this has already been written as rust-lang/rust#24133.