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 theNoneErrortype:
/// 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 convertEto 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
SearchResulttype:
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?
I need some time to think. Thank you again!