Hello everyone!
So I was taking a look at the std::ops::Try
that allows to use ?
with custom types by implementing that trait, and after noticing it was not stabilized yet, I read some of the comments from the tracking issue that were raising a few drawbacks of the current trait.
Mainly, the separate from_ok
and from_error
methods, the naming that is very skewed towards the error handling use case, or how it is difficult to implement the trait for a type that doesn't map directly to result.
So, I'm suggesting another trait that's a bit different from Try
, in the hope that it would be easier to implement for users, and would cover a slightly larger use-case.
The initial inspiration for the trait was looking for a naming that was less tangled with the idea of "trying" something. So I tried to find a naming more descriptive of what the operator is doing, and I thought that instead of relying on Result
, the trait should use a different, specialized enum:
#[must_use]
pub enum ExtractOrReturn<Early, Extract> {
/// Return early with value of type Early
ReturnEarly(Early),
/// Extract value of type Extract
Extract(Extract),
}
Like std::cmp::Ordering
is a specialized enum to express the result of a comparison operator, ExtractOrReturn
is a specialized enum to express the result of the ?
operator, where the ReturnEarly
variant is used when it should return early, and the extract
variant when it should produce an expression.
This enum made me realize how asymetric the ?
operator really is.
When "extractring" a value from expression e
in a let x = e?;
statement, there should be only a single type that e
can be extracted to.
However, since we want easy casting to the return type of the function, a expression e
of type T
typically needs to be convertible to various return type when returning early in a e?
expression.
This consideration leads me to the following trait:
/// Trait to implement to be able to use the `q!` macro on an instance of the type
///
/// A type implementing this trait defines how an instance of this type
/// can be either extracted to the Extract type, or returned early as an instance
/// of the Early type.
pub trait Question<Early> {
/// The type that should be extracted
type Extract;
/// A function to determine if the expression x? should either extract
/// a value from x of type Extract, or return early a value of type Early.
/// To do so, this function returns either ReturnEarly(early), or
/// Extract(extract).
fn extract_or_return(self) -> ExtractOrReturn<Early, Self::Extract>;
}
In the trait above, the type parameter Early
allows to implement the trait for a variety of function return types, while the associated type Extract
ensures that a unique type is extracted from the expression in the e?
(for a function with a given return type, at least).
While the naming is not ideal (the std::ops::Sub
trait is not named std::ops::MinusSign
, so I shouldn't have a trait named std::ops::Question(Mark)
, I feel like implementing this trait for types is generally easier than for Try
.
- The implementation for
Option<T>
does not need theNoneError
type:
/// Extract T from Option<T>, or return None early.
impl<T, U> Question<Option<U>> for Option<T> {
type Extract = T;
fn extract_or_return(self) -> ExtractOrReturn<Option<U>, T> {
match self {
Some(u) => ExtractOrReturn::Extract(u),
None => ExtractOrReturn::ReturnEarly(None),
}
}
}
- The implementation for
Result<T, E>
retains the ability to convertE
to a compatible error typeF
:
/// Extract T from Result<T, E>, or convert its error to return
/// a Result<U, F> early.
impl<T, U, E, F> Question<Result<U, F>> for Result<T, E>
where
F: std::convert::From<E>,
{
type Extract = T;
fn extract_or_return(self) -> ExtractOrReturn<Result<U, F>, T> {
match self {
Ok(ok) => ExtractOrReturn::Extract(ok),
Err(err) => ExtractOrReturn::ReturnEarly(Err(err.into())),
}
}
}
Here is the implementation for the enum of @nikomatsakis:
enum StrandFail<T> {
Success(T),
NoSolution,
QuantumExceeded,
Cycle(Strand, Minimums),
}
impl<T> Question<StrandFail<T>> for StrandFail<T> {
type Extract = T;
fn extract_or_return(self) -> ExtractOrReturn<Self, T> {
match self {
StrandFail::Success(t) => ExtractOrReturn::Extract(t),
other => ExtractOrReturn::ReturnEarly(other),
}
}
}
No need for a helper type anymore!
- Here is @skade's
SearchResult
type:
enum SearchResult<T> {
Found(T),
NotFound,
}
impl<T> Question<SearchResult<T>> for SearchResult<T> {
type Extract = SearchResult<T>;
fn extract_or_return(self) -> ExtractOrReturn<Self, Self> {
match self {
SearchResult::Found(_) => ExtractOrReturn::ReturnEarly(self),
SearchResult::NotFound => ExtractOrReturn::Extract(self), // keep searching
}
}
}
fn fun() -> SearchResult<Socks> {
search_drawer()?; // keeps searching or return early Found(socks)
search_wardrobe() // Either Found(Socks) or NotFound
}
To test this idea, I have set up a small repository containing the trait, various example implementations, and a q!
macro that is a stand-in for the ?
operator (since the Question
semantics is not implementable in terms of the Try
trait).
The implementation of the q!
macros is the following, and could be an alternative semantics for the ?
operator:
#[macro_export]
macro_rules! q {
($e:expr) => {
match $crate::Question::<_>::extract_or_return($e) {
$crate::ExtractOrReturn::ReturnEarly(early) => return early,
$crate::ExtractOrReturn::Extract(extract) => extract,
}
};
}
If you followed along this post, thank you for reading all this !
What do you think of this idea? Is there anything I'm missing, or does this Question
trait constitutes an alternative to the Try
trait? Do you agree that it looks easier to implement and just a bit more general? It looks like we are really late in the implementation process of the Try
trait, is it too late to propose such a "big" change? Should it be worth it, should there be a RFC?
What do you think?