Carrier trait for testing, DON'T LAND by nrc · Pull Request #35056 · rust-lang/rust (original) (raw)

OK, so I want to lay out my thinking on this change.

First off, I thought the idea of introducing this Carrier trait with dummy type was to safeguard -- in particular, we wanted to stabilize ? without having to stabilize its interaction with type inference. This combination of trait plus dummy type seemed like it would be safely conservative.

The idea was (I think) that we would then write-up an RFC discussing Carrier and try to modify the design to match, stabilizing only when we were happy with the overall shape (or possibly removing Carrier altogether, if we can't reach a design we like).

Now, speaking a bit more speculatively, I anticipate that, if we do adopt a Carrier trait, we would want to disallow interconversion between carriers (whereas this trait is basically a way to convert to/from Result). So intuitively if you apply ? to an Option, that's ok if the fn returns Option; and if you apply ? to a Result<T,E>, that's ok if the fn returns Result<U,F> where E: Into<F>; but if you apply ? to an Option and the fn returns Result<..>, that's not ok.

That said, this sort of rule is hard to express in today's type system. The most obvious starting point would be something like HKT (which of course we don't really have, but let's ignore that for now). However, that's not obviously perfect. If we were to use it, one would assume that the Self parameter for Carrier has kind type -> type -> type, since Result can implement Carrier. That would allow us to express things like Self<T,E> -> Self<U,F>. However, it would not necessarily apply to Option, which has kind type -> type (all of this would of course depend on precisely what sort of HKT system we adopted, but I don't think we'll go all the way to "general type lambdas"). Even more extreme might be a type like bool (although I don't want to implement Carrier for bool, I would expect some people might want to implement Carrier for a newtype'd bool).

What I had considered is that the typing rules for ? could themselves be special: for example, we might say that ? can only be applied to a nominal type Foo<..> of some kind, and that it will match the Carrier trait against this type, but it will require that the return type of the enclosing fn is also Foo<..>. So we would basically instantiate Foo with fresh type parameters. The downside of this idea is that if neither the type that ? is being applied to nor the type of the enclosing fn is known, we can't enforce this constraint without adding some new kind of trait obligation. It's also rather ad-hoc. :) But it would work.

Another thought I had is that we might reconsider the Carrier trait. The idea would be to have Expr: Carrier<Return> where Expr is the type ? is applied to and Return is the type of the environment. For example, perhaps it might look like this:

trait Carrier { type Ok; fn is_ok(&self) -> bool; // if true, represents the "ok-like" variant fn unwrap_into_ok(self) -> Self::Ok; // may panic if not ok fn unwrap_into_error(self) -> Target; // may panic if not error }

Then expr? desugars to:

let val = expr; if Carrier::is_ok(&val) { val.unwrap_into_ok() } else { return val.unwrap_into_error(); }

The key difference here is that Target would not be the error type, but a new Result type. So for example we might add the following impl:

impl<T,U,E,F> Carrier<Result<U,F>> for Result<T,E> where E: Into { type Ok = T; fn is_ok(&self) -> bool { self.is_ok() } fn unwrap_into_ok(self) -> Self::Ok { self.unwrap() } fn unwrap_into_error(self) -> { Err(F::from(self.unwrap_err())) } }

And then we might add:

impl Carrier<Option> for Option { type Ok = T; fn is_ok(&self) -> bool { self.is_some() } fn unwrap_into_ok(self) -> Self::Ok { self.unwrap() } fn unwrap_into_error(self) -> { debug_assert!(self.is_none()); None } }

And finally we could implement for bool like so:

struct MyBool(bool); impl Carrier for MyBool { type Ok = (); fn is_ok(&self) -> bool { self.0 } fn unwrap_into_ok(self) -> Self::Ok { debug_assert!(self.0); () } fn unwrap_into_error(self) -> { debug_assert!(!self.0); self } }

Now this version is more flexible. For example, we could allow interconversion between Option values to be converted to Result by adding an impl like:

impl Carrier<Result<T,()>> for Option { ... }

But of course we don't have to (and we wouldn't).