-
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
Carrier trait for ?
operator
#1718
Comments
https://internals.rust-lang.org/t/the-operator-and-a-carrier-trait/ probably work linking too |
I am still a fan of @Stebalien's variant on my original proposal: enum Carrier<C, R> {
Continue(C),
Return(R),
}
trait IntoCarrier<Return> {
type Continue;
fn into_carrier(self) -> Carrier<Self::Continue, Return>;
} Coupled with the "design guidelines" that one should only use the impl<T,U,E,F> IntoCarrier<Result<U,F>> for Result<T,E> where E: Into<F> {
type Continue = T;
} // good
impl<T> IntoCarrier<Option<T>> for Option<T> {
type Continue = T;
} // good But this would be considered an anti-pattern: impl<T> IntoCarrier<T, Result<T,()>> for Option<T> { } // bad |
I really like this carrier proposal and I would vote in favor of supporting a carrier for option. One thing that I keep running into as a weirder example currently is the common case of a result being wrapped in an option. This is something that shows up in the context of iterators frequently. A good example is the walkdir crate. From what I can tell Rust could already express a generic implementation for that case however I wanted to raise this as something to consider. The case I can see third party libraries implement is a conversion from future to result. What I like about the IntoCarrier is that - if I understand it correctly - it could be implemented both ways by a third party crate: future to result and result to future. |
I implemented a few variants of carrier here to see how it works: https://gist.github.com/mitsuhiko/51177b5bf1ebe81cfdd36946a249bba3 I really like that this would permit error handling within iterators. It's much better than what I did in the past where I had to make a manual |
@mitsuhiko ah, that's a nice idea to prototype it like that. I also did some experiments in play around type inference and The idea of 'throwing' a result error into I think we will certainly see some other cases where you want to interconvert between "related" types or patterns that have a known semantic meaning. I also expect that we will get a certain amount of pressure to interconvert |
I don't want to start a bikeshed here but I want to throw up some ideas for making the carrier a bit less confusing to people. The carrier primarily exists internally but I assume the docs will sooner or later explain Right now it's What about it being called a
Huge plus + 1 on not adding this. I was originally on the side of supporting this but the more I play around with this and |
I also find the Carrier name undesirable. On Fri, Aug 19, 2016, 8:26 AM Armin Ronacher notifications@github.com
|
I dislike the name |
Joining everyone in the bikeshed, I agree that Also I'm not a fan of
|
@nikomatsakis @Stebalien I don't think a |
@ticki But it's not about success/failure. For example, an iterator returning |
@nikomatsakis the main argument for @ticki overloading result is not great, because it overloads the type a lot with API that users should not be exposed to. I already fear that someone going to the result docs is overwhelmed by how much API there is. (Also it would get super confusing if error handling in iterators is considered like I had in my gist) |
@mitsuhiko "Outcome" seems too much like "Result". More generally, though, I think we should avoid bikeshedding the name until we agree on the semantics. Once we define the semantics, we'll have a better idea of the right name for them. |
@nikomatsakis @Stebalien Is the intent of the separate |
Repeating myself from the internals thread: I now think full Monad support is less challenging than it looks, and am wary about stabilizing any trait for this before HKTs lands. |
@Ericson2314 As long as it doesn't involve closures, I agree that something more like |
@Ericson2314: I am intrigued and surprised, as up until this point I was not aware that there was any ongoing development (in the language or elsewhere) that would bring the language closer towards HKTs. Hopefully without derailing the thread too much, is there a place where one can read more about this? |
@eddyb It's still my closure plan :). Adapting https://internals.rust-lang.org/t/autoclosures-via-swift/3616/52: This let x = do {
loop {
let y = monadic_action()?;
if !is_ok(y) {
break Monad::return(None);
}
normal_action();
}
}; get's desugared into let x = {
let edge0 = late |()| monadic_action().bind(edge1);
let edge1 = late |temp0| {
let y = temp0;
if !is_ok(y) {
drop(y);
Monad::return(None)
} else {
normal_action();
drop(y);
Monad::return(()).bind(edge0)
}
};
edge0(())
}; The In any event others have ideas in this arena, and we already want to make nice "synchronous" sugar for futures-rs. I believe the future monad has a sufficiently complex representation that anything that works for it ought to generalize for monads in general. |
@Ericson2314 FWIW, LLVM is getting native support for coroutines, and monadic do can be implemented as a wrapper around that, which may or may not be more efficient - it constructs one big function for the whole coroutine (do block) with an entry point that jumps to the appropriate place based on the current state, rather than splitting it across multiple closures. The biggest issue - and one not covered by the future monad, incidentally, or by your desugaring per se - is copying the function state in order to call the callback passed to |
@comex IMO LLVM's coroutines are the completely wrong level for Rust, they fundamentally rely on LLVM's ability to optimize out heap allocations. We can do much better on the MIR and with ADTs. |
Niko's examples of how to use IntoCarrier didn't seem to work, so I figured I'd try implementing it: enum Carrier<C, R> {
Continue(C),
Return(R),
}
trait IntoCarrier<Return> {
type Continue;
fn into_carrier(self) -> Carrier<Self::Continue, Return>;
}
// impl for Result
impl<T, E, F> IntoCarrier<Result<T, F>> for Result<T, E> where E: Into<F> {
type Continue = T;
fn into_carrier(self) -> Carrier<Self::Continue, Result<T, F>> {
match self {
Ok(v) => Carrier::Continue(v),
Err(e) => Carrier::Return(Err(e.into())),
}
}
}
// impl for Option
impl<T> IntoCarrier<Option<T>> for Option<T> {
type Continue = T;
fn into_carrier(self) -> Carrier<Self::Continue, Option<T>> {
match self {
Some(s) => Carrier::Continue(s),
None => Carrier::Return(None),
}
}
}
// impl for bool (just because)
impl IntoCarrier<bool> for bool {
type Continue = bool;
fn into_carrier(self) -> Carrier<Self::Continue, bool> {
if self { Carrier::Continue(self) } else { Carrier::Return(self) }
}
}
// anti-pattern
impl<T> IntoCarrier<Result<T, ()>> for Option<T> {
type Continue = T;
fn into_carrier(self) -> Carrier<Self::Continue, Result<T, ()>> {
match self {
Some(s) => Carrier::Continue(s),
None => Carrier::Return(Err(())),
}
}
}
macro_rules! t {
($expr:expr) => (match IntoCarrier::into_carrier($expr) {
Carrier::Continue(v) => v,
Carrier::Return(e) => return e,
})
}
fn to_result(ok: bool, v: i32, e: u32) -> Result<i32, u32> {
if ok { Ok(v) } else { Err(e) }
}
fn test_result(ok: bool) -> Result<i32, u32> {
let i: i32 = t!(to_result(ok, -1, 1));
Ok(i)
}
fn to_option(some: bool, v: i32) -> Option<i32> {
if some { Some(v) } else { None }
}
fn test_option(some: bool) -> Option<i32> {
let i: i32 = t!(to_option(some, -1));
Some(i)
}
fn to_bool(b: bool) -> bool { b }
fn test_bool(b: bool) -> bool {
let i: bool = t!(to_bool(b));
i
}
fn to_antipattern(some: bool, v: i32) -> Option<i32> {
if some { Some(v) } else { None }
}
fn test_antipattern(ok: bool) -> Result<i32, ()> {
let i: i32 = t!(to_antipattern(ok, -1));
Ok(i)
}
fn main() {
assert_eq!(test_result(true), Ok(-1i32));
assert_eq!(test_result(false), Err(1u32));
assert_eq!(test_option(true), Some(-1i32));
assert_eq!(test_option(false), None);
assert_eq!(test_bool(true), true);
assert_eq!(test_bool(false), false);
assert_eq!(test_antipattern(true), Ok(-1i32));
assert_eq!(test_antipattern(false), Err(()));
} |
@eddyb hmm, I didn't pay much attention to the allocation stuff when I was skimming the coroutine docs. I guess that's a poor match as is... and I can see why changing it would be hard. Even so, fundamentally, I'm skeptical that the best design is one that takes away much of the LLVM optimizer's knowledge of the control flow graph, if there is an alternative that does not. MIR optimizations are nice but they aren't everything. |
@comex Being skeptical is good 😄. I honestly don't know yet just how far we can take it, experiments are most definitely needed before we can settle on one mechanism - maybe neither is as great on their own as would be a hybrid, who knows? I only have opinions and speculations so far, no hard data yet. |
Carrier trait (third attempt) This adds a `Carrier` trait to operate with `?`. The only public implementation is for `Result`, so effectively the trait does not exist, however, it ensures future compatibility for the `?` operator. This is not intended to be used, nor is it intended to be a long-term solution. Although this exact PR has not been through Crater, I do not expect it to be a breaking change based on putting numerous similar PRs though Crater in the past. cc: * [? tracking issue](#31436) * [previous PR](#35056) * [RFC issue](rust-lang/rfcs#1718) for discussion of long-term Carrier trait solutions. r? @nikomatsakis
I started a repository here to explore with the carriers more: https://github.com/mitsuhiko/rust-carrier Primarily a few things I noticed so far:
WRT the Monad discussion: I cannot express enough how I would like any HKT or Monad discussion not to take place here. The concept is already hard enough and it should be used by as many users as possible. The code examples shown here demonstrate quite well the problems this will cause for actually explaining the error handling to mere mortals. |
I like the term |
Who is eligible to vote for the trait's name? If it matters, I like |
I'll try to draw up this RFC ASAP. I'm OK with either |
Actually, if anyone is interested in working on the RFC jointly, please contact me! This seems like a good opportunity to get into the RFC process. |
Shouldn't the |
Assuming this remains the proposal,
I'd imagine all |
This #1718 (comment) is the most recent proposal. |
I see, so the trait normally being used is Why does it "not work so well with the existing inference"? Are issues with these generic
Or simply that error forgetting
|
Copy-pasting my response from the internals since I didn't see this thread: I'm against Note that this is already the case: Seeing as |
The most recent suggestion appears to work with my main use case, so I am excited to see the RFC! #[derive(Debug)]
struct Status<L, T, F> {
location: L,
kind: StatusKind<T, F>,
}
#[derive(Debug)]
enum StatusKind<T, F> {
Success(T),
Failure(F),
} Using the return type of |
I brought this up in the futures crate, but the design also seems relevant for here: considering the pattern from futures of having The futures trait could possibly change such that An alternative idea is for this trait to not return In the futures case, there is desire to return when the value trait Try<C, R> {
fn try(self) -> Tried<C, R>;
} An implementation for the futures crate: impl<C, R> Try<C, Poll<C, R>> for Poll<C, R> {
fn try(self) -> Tried<C, Poll<C, R>> {
match self {
Ok(Async::Ready(val)) => Tried::Continue(val),
other => Tried::Return(other),
}
}
} De-sugaring of let val = match Try::try(self.inner.poll()) {
Tried::Continue(val) => val,
Tried::Return(val) => return val, // or catch { ... }
} |
I actually find |
@seanmonstar It seems to me that the futures crate would indeed be better off defining its own type, rather than using an actual |
Note that the reason futures return |
Here is a draft of the |
Poll could also just be a typedef for |
@nikomatsakis oh some interesting thoughts:
I forgot a case actually but we actually did want precisely this. When implementing Basically I forgot that All that's just to say that this may not be the best example, and does kinda throw doubt in my mind as to whether we'd switch to |
@alexcrichton To be clear, the impl you want is this? That is indeed a violation of the orphan rules (right now at least). |
@withoutboats morally, yeah that's what we'd want. One of the current motivations for We may not want to put too much weight on the design of futures today, though. We haven't really exhaustively explored our options here so this may just be too much in the realm of "futures specific problems". |
I think the RFC needs an explanation of why this uses |
@alexcrichton Yea I don't think this trait should change because of that issue with futures! But I do think its an interesting example of the orphan rules being more restrictive than we'd like, & (unlike many examples) its a way I think we could consider changing. That example is disallowed to allow std to |
Hmm, so the current setup makes the I agree with @withoutboats that this is a shortcoming of the orphan rules. In particular, if the types were not generic, you actually could do |
I'll add some text. To be honest, I don't care too much one way or the other about this specific point. |
Updated draft RFC slightly (same url). I was just re-reading it though, and I realize that in the RFC text itself I give an example of converting a Are there any use-cases for going the other way that we know of? (i.e., converting from a |
@nikomatsakis looks good to me! |
Posted as #1859. |
We accepted an RFC to add the
?
operator, that RFC included aCarrier
trait to allow applying?
to types other thanResult
, e.g.,Option
. However, we accepted a version of the RFC without that in order to get some actual movement and some experience with the?
operator.The bare
?
operator is now on the road to stabilisation. We would like to consider adding aCarrier
trait and that will require a new RFC. There is in fact a 'dummy' Carrier trait in libcore and used in the implementation of?
. However, its purpose is to ensure?
is backwards compatible around type inference. It is effectively only implemented forResult
and is not intended to be a long-term solution. There are other options for the trait using HKT, variant types, and just existing features. It's unclear what is preferred right now.One important question is whether we should allow conversion between types (e.g.,
Result
toOption
) or whether we should only allow the?
operator to work on one type 'at a time'.Links:
? operator RFC PR
? operator tracking issue
Niko's thoughts
The text was updated successfully, but these errors were encountered: