Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upCarrier trait for `?` operator #1718
Comments
nrc
added
the
T-lang
label
Aug 18, 2016
This comment has been minimized.
This comment has been minimized.
|
https://internals.rust-lang.org/t/the-operator-and-a-carrier-trait/ probably work linking too |
This comment has been minimized.
This comment has been minimized.
|
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;
} // goodBut this would be considered an anti-pattern: impl<T> IntoCarrier<T, Result<T,()>> for Option<T> { } // bad |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
I also find the Carrier name undesirable. On Fri, Aug 19, 2016, 8:26 AM Armin Ronacher notifications@github.com
|
This comment has been minimized.
This comment has been minimized.
|
I dislike the name |
This comment has been minimized.
This comment has been minimized.
|
Joining everyone in the bikeshed, I agree that Also I'm not a fan of
|
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis @Stebalien I don't think a |
This comment has been minimized.
This comment has been minimized.
|
@ticki But it's not about success/failure. For example, an iterator returning |
This comment has been minimized.
This comment has been minimized.
|
@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) |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis @Stebalien Is the intent of the separate |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 As long as it doesn't involve closures, I agree that something more like |
This comment has been minimized.
This comment has been minimized.
ExpHP
commented
Aug 20, 2016
|
@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? |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
comex
commented
Aug 21, 2016
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
ahmedcharles
commented
Aug 21, 2016
|
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(()));
} |
This comment has been minimized.
This comment has been minimized.
comex
commented
Aug 21, 2016
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
@comex Being skeptical is good |
bors
added a commit
to rust-lang/rust
that referenced
this issue
Aug 21, 2016
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
I like the term |
This comment has been minimized.
This comment has been minimized.
norru
commented
Jan 4, 2017
•
|
Who is eligible to vote for the trait's name? If it matters, I like |
This comment has been minimized.
This comment has been minimized.
|
I'll try to draw up this RFC ASAP. I'm OK with either |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
Wyverald
commented
Jan 8, 2017
|
Shouldn't the |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jan 8, 2017
|
Assuming this remains the proposal,
I'd imagine all |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Jan 8, 2017
|
This #1718 (comment) is the most recent proposal. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jan 8, 2017
|
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
|
This comment has been minimized.
This comment has been minimized.
mglagla
commented
Jan 12, 2017
|
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 |
seanmonstar
referenced this issue
Jan 14, 2017
Closed
Rationalize conversions throughout combinators #291
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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 { ... }
} |
This comment has been minimized.
This comment has been minimized.
I actually find |
This comment has been minimized.
This comment has been minimized.
|
@seanmonstar It seems to me that the futures crate would indeed be better off defining its own type, rather than using an actual |
This comment has been minimized.
This comment has been minimized.
|
Note that the reason futures return |
This comment has been minimized.
This comment has been minimized.
|
Here is a draft of the |
This comment has been minimized.
This comment has been minimized.
|
Poll could also just be a typedef for |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
@alexcrichton To be clear, the impl you want is this? That is indeed a violation of the orphan rules (right now at least). |
This comment has been minimized.
This comment has been minimized.
|
@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". |
This comment has been minimized.
This comment has been minimized.
|
I think the RFC needs an explanation of why this uses |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
I'll add some text. To be honest, I don't care too much one way or the other about this specific point. |
This comment has been minimized.
This comment has been minimized.
|
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
referenced this issue
Jan 19, 2017
Open
Orphan rules are stricter than we would like #1856
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis looks good to me! |
This comment has been minimized.
This comment has been minimized.
|
Posted as #1859. |
nrc commentedAug 18, 2016
We accepted an RFC to add the
?operator, that RFC included aCarriertrait 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 aCarriertrait 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 forResultand 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.,
ResulttoOption) 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