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).