diff --git a/Cargo.toml b/Cargo.toml index 9648e27..84aa26e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ proc_macro2_diagnostic = "0.3.0" quote = "1.0.45" [dev-dependencies] +log = "0.4.29" trybuild = "1.0.116" [build-dependencies] diff --git a/README.md b/README.md index e896732..5032102 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - must have _at least one_ generic type - the _first_ generic type must be the `Output` type (produced when not short-circuiting) - the output variant (does not short-circuit) must be the _first_ variant and store the output type as the _only unnamed_ field +- no other variant can store the Output type (TODO #72 add a nice error message) See the [full documentation](https://docs.rs/try_v2/latest/try_v2/) for specifics on the generated code. diff --git a/docs/hacking.md b/docs/hacking.md new file mode 100644 index 0000000..315c7ea --- /dev/null +++ b/docs/hacking.md @@ -0,0 +1,553 @@ +# Thoughts on `try_trait_v2` + +Let me start off by saying, I _love_ `trait Try` and really hope to see it accepted soon. I'm happy (and starting) to help with moving that forwards too. + +## Emotionally + +I like the trait for two reasons: + +1. I don't like it when std can do stuff I can't, `Try` opens up the power and versatility of `?` to my own code. +1. I don't like writing extra code when it feels like I'm just working _around_ language constraints. `Try` lets me add `impl`s directly to a custom `Result` type or flatten nested constructs inside `Option`s & `Result`s + +## My related crates + +To date I've thought about using the trait multiple times but always found I would end up with more code than simply working around `Option` & `Result`. Then I ran into a case where I **absolutely had to** add a trait to a `Result` - I wanted something to return from `fn main()` which gave me control over exit codes, ensured `Drop` was run properly _and_ didn't leave me with go-like error handling in `main` :feelsgood: + +### exit_safely + +[MusicalNinjaDad/exit_safely](https://github.com/MusicalNinjaDad/exit_safely) works with derived Try implementations via [MusicalNinjaDad/try_v2](https://github.com/MusicalNinjaDad/try_v2) to solve the problem of returning from main with Drop and control over exit codes. + +### proc_macro2_diagnostic + +[MusicalNinjaDad/proc_macro2_diagnostic](https://github.com/MusicalNinjaDad/proc_macro2_diagnostic) brings `?` to compiler diagnostics for proc macros. + +### try_v2 + +[MusicalNinjaDad/try_v2](https://github.com/MusicalNinjaDad/try_v2) provides a set of derive macros to make `Try` more accessible. (See below for details) + +## (Invalid) criticism: complexity + +After working with the trait in various use cases, taking it apart to try (!) and derive a generic implementation and spending time reading RFCs, unstable books, comments in std source code, github issues, PRs, discord discussions, ... to my mind, the remaining complexity in `Try` is **inherent**. The implementation is **as simple as possible** to provide the power and flexibility required for the more **meaningful use cases**. + +## The 3 traits + 1 type + 1 function + +When talking about `Try` below, I will usually consider the following traits in one package (in order of importance for implementing Try): + +- `trait Try` (`try_trait_v2`) +- `trait FromResidual` (`try_trait_v2`), Try is unusable without this +- `trait Residual` (`try_trait_v2_residual`), Invaluable for working generically on a Try type +- `type !` (`never_type`), Saves a bucket-load of keypresses +- `fn try_collect()` (`iterator_try_collect`), very-nice-to-have & makes `#[derive(Try_Iterator)]` possible + +### 2 more experimental features + +As weird as it may be from the naming `try_blocks`, `try_blocks_heterogeneous` are more separate from a usage point of view. + +## Simple case + +Std contains 3 types which impl Try for all cases: `Result`, `Option` & `ControlFlow`. Many of the most obvious uses for Try involve `Result`-like or `Option`-like situations and it is usually possible, if a little verbose / annoying, to work with `Result` & `Option` to get the same result (!). + +### Flattening nested types + +```rust +use std::{error::Error, io}; + +/// I _might_ have found somewhere that could contain duplicate info +/// Identifying duplicates _might_ have caused an error +/// And the answer _might_ be "no overlap" +type DuplicateData = Option, ValidErrors>>; + +enum ValidErrors { + Parsing(Box), + IO(Box), +} + +// There is no good way to unpack this for use, or pass it "up the chain" for handling without +// repeated let Some(Ok(Some()))-else-return directly in the code each time :( + +fn main() { + fn process(foo: DuplicateData) -> DuplicateData { + // This looks very much like: + // bar, err := foo + // if err != nil { + // return err + // } + let Some(Ok(Some(bar))) = foo else { + return foo; + }; + + let baz = bar + 1; + Some(Ok(Some(baz))) + } + let foo: DuplicateData = Some(Ok(Some(5))); + assert!(matches!(process(foo), Some(Ok(Some(6))))); +} +``` + +Wouldn't it be nicer to be able to have everything in one place, with meaningful names for when I do want to handle non-`Some(Ok(Some(_)))` values? + +This is much easier to create, read, and reason about: + +```rust +use std::{error::Error, io}; + +use try_v2::Try; + +use DuplicateData::Duplicate; + +#[derive(Debug, Try)] +#[must_use] +enum DuplicateData { + Duplicate(T), + NoCandidate, + NoDuplicates, + ParsingError(Box), + IOError(Box), +} + +fn main() { + fn process(foo: DuplicateData) -> DuplicateData { + let baz = foo? + 1; + Duplicate(baz) + } + let foo: DuplicateData = Duplicate(5); + assert!(matches!(process(foo), Duplicate(6))); +} +``` + +### Flattening trait MyFunctionsExt + +Type-alias-ing a `Result` or `Option` is current idiom but ... + +Adding methods and functions to a Type alias requires a custom trait, which adds an annoying copy of all the signatures that you need to keep up to date. It also pushes an implementation detail into the downstream code, if for example the functionality shadows a std or well-known 3rd-party trait... + +The alternative is a NewType, which again pushes an implementation detail into downstream code which now needs to use `MyType.0`. Even worse, this adds a subtle nudge for people to `impl Deref` on their NewType - which is specifically _not_ designed for this kind of ergonomic hack. + +```rust +use log::info; + +/// A Counter which logs each adjustment +type Counter = Option; + +// While not so much extra work to double-specify everything as a trait & impl, it still +// wastes keystrokes and +trait NumberExt { + fn new() -> Self; + fn inc(self, n: N) -> Self; + // etc... +} + +impl NumberExt for Counter { + fn new() -> Self { + info!("new counter initialised"); + Some(0) + } + + // Not possible to impl std::ops::Add on a type alias, which leads to some people `impl Deref` + // to avoid peppering code with `.0` for a NewType or working around it in other ways, like this. + // + // Both cases force consideration of the implementation details in the code which uses Counter. + fn inc(self, n: i32) -> Self { + info!("adding {n} to counter"); + let n = self? + n; + info!("new value {n}"); + Some(n) + } +} + +fn main() { + let foo = Counter::new(); + assert!(matches!(foo.inc(2), Some(2))); +} +``` + +With try this becomes much nicer to implement, read and use: + +```rust +use log::info; +use std::{fmt::Display, ops::Add}; + +use try_v2::Try; + +use Counter::Count; + +#[derive(Debug, Try)] +#[must_use] +enum Counter { + Count(N), + Uninitialised, +} + +impl Counter { + fn new() -> Self { + info!("new counter initialised"); + Count(0) + } +} + +// More versatile implementation, better separating responsibilities: +// implementation details are owned here, type specifics at usage site. +impl Add for Counter +where + N: Add + Display, + M: Display, +{ + type Output = Self; + + fn add(self, rhs: M) -> Self::Output { + info!("adding {rhs} to counter"); + let n = self? + rhs; + info!("new value {n}"); + Self::Count(n) + } +} + +fn main() { + let foo = Counter::new(); + assert!(matches!(foo + 2, Count(n) if n ==2)); +} +``` + +## Boilerplate & Gotchas -> Derive + +While the above is really easy to create, use and reason about, manually implementing Try for this comes with a chunk of boilerplate code and a few gotchas. Getting the same ergonomics that are available from Option, Try & Control-Flow adds even more boilerplate. As such the pay off was never there for me personally, until I was _forced_ to put a minimal implementation in place for [exit_safely](https://crates.io/crates/exit_safely). In true [pass-the-salt](https://xkcd.com/974/) style I went ahead and created the derive macros in [try_v2](https://crates.io/crates/try_v2). + +Note: I'd be happy to pass some (or all) of these into std, while leaving the "nice-to-haves" in a separate crate, if that would be valuable. + +### Derivable case + +The simple case described above is derivable (as in the examples) and is probably the most (numerically) common expected usage of Try. To be able to guarantee the `Foo` pattern for the `Residual` and algorithmically generate arms for `branch()`, `from_residual()` etc. the macro enforces a few invariants on the annotated type: + +- must be an `enum` +- must have _at least one_ generic type +- the _first_ generic type must be the `Output` type (produced when not short-circuiting) +- the output variant (does not short-circuit) must be the _first_ variant and store the output type as the _only unnamed_ field +- no other variant can store the Output type (see #72 "add a nice error message") + +While technically, the generic ordering requirement could be relaxed with slightly more complex logic, it is [deliberately tight](https://en.wikipedia.org/wiki/Poka-yoke) - to avoid accidental, and hard to spot, mistakes caused by switching generics. + +### Derive Example + +For the following case (based upon the usage in [pt](https://github.com/MusicalNinjaDad/pt)) + +```rust +#[derive(Try, Try_ConvertResult, Try_Iterator, Try_Methods)] +enum TestResult { + Ok(T), + TestsFailed, + OtherError(E) +} +``` + +### Macro `Try`: derives `Try`, `FromResidual` and `Residual` + +will result in code of the shape: + +```rust +impl Try for TestResult { + type Output = T; + type Residual = TestResult; + + fn from_output(output: T) -> Self { + Self::Ok(output) + } + + fn branch(self) -> ControlFlow { + Self::Ok(t) => Continue(t), + ... each failing variant => Break(failing variant) ... + } +} + +impl FromResidual> for TestResult { + fn from_residual(residual: TestResult) -> Self { + match residual { + ... each failing variant => itself ... + } + } +} + +impl Residual for TestResult { + type TryType = TestResult; +} +``` + +### Macro `Try_ConvertResult`: derives bidirectional `FromResidual` with `Result` + +adds + +```rust + +impl FromResidual> for TestResult +where + RE: Into> + +... which calls Result::Err(e) => e.into(), ... +``` + +and + +```rust +impl FromResidual> for Result +where + RE: From> + +... which calls Result::Err(residual.into()) ... +``` + +#### Why require `From/Into Foo` and not `Foo<_>`? + +2 reasons: + +1. Otherwise you cannot create a non-conflicting implementation to allow for functions returning `Result>` to be ?-ed in functions returning `MyTry` +2. It stops people from accidentally returning a `Result::Err(TestResult::Ok)` ([Poka-Yoke](https://en.wikipedia.org/wiki/Poka-yoke) again). If you actually want this ... don't derive as you probably need specific logic to handle this case. + +Effectively that allows using your type in any trait function where a `Result` is expected. Here's the `TryFrom` example from the integration tests. The subtle point to note: `let n = Even::try_from(num)?;` uses `?` to provide an `Even`, in a function that aims to return `Eightball`, not `Eightball` + +```rust +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] + +use try_v2::{Try, Try_ConvertResult}; + +#[derive(Try, Try_ConvertResult)] +#[must_use] +enum Eightball { + Yes(Y), + No, +} + +struct Even(i32); + +impl TryFrom for Even { + type Error = Eightball; + + fn try_from(num: i32) -> Result> { + if num % 2 == 0 { + Result::Ok(Even(num)) + } else { + Result::Err(Eightball::No) + } + } +} + +fn even_string(num: i32) -> Eightball { + let n = Even::try_from(num)?; + let s = format!("{}", n.0); + Eightball::Yes(s) +} + +assert!(matches!(even_string(2), Eightball::Yes(s) if s == "2")); +assert!(matches!(even_string(1), Eightball::No)); +``` + +### Macro `Try_Iterator`: derives `IntoIterator` and `FromIterator` analog to `Result` & `Option` + +The stdlib implementations are almost identical. I took a lazy approach and have leveraged `std::option::IntoIter` to allow: + +```rust +let tests: Vec> = vec![Ok(1), TestsFailed, Ok(2), OtherError("something weird"), Ok(3), Ok(4)]; + +let first_results: TestResult, &'static str> = tests.into_iter().collect(); +assert!(matches!(first_results, TestsFailed)); + +let mut test: TestResult = Ok(4); +let borrowed_result: &i32 = test.iter().next().unwrap(); +assert_eq!(borrowed_result, &4); +match test.iter_mut().next() { + Some(v) => *v = 5, + None => {}, +} +assert!(matches!(test, TestResult::Ok(v) if v == 5)); +let result = test.into_iter().next(); +assert_eq!(result, Some(5)); +``` + +### Macro `Try_Methods` (WIP): derives `unwrap()` + +`Option` & `Result` have a large set of semantically overlapping ergonomic methods for: + +- Querying the variant +- Adapters for working with references (only `Option`) +- Extracting contained values +- Transforming contained values +- Boolean operators + +Current task in progress is to derive equivalent methods named according to the enum variants. Right now, I have unwrap, goal is `is_testfailed()`, `expect()`, `othererror_or_else()`, `map_othererror` etc. + +### Gotcha!s -> Derive + +There are a few "gotcha!s" with even the simple implementation which can be easily avoided by careful documentation, reading & thinking ... or deriving. + +- Interconversion with Result: overlapping `Into` impls. +This one bit me in the ass when using my own macros - while it may feel slightly awkward to require conversion to & from Result<_, MyResidual> anything else can trip you up later and be a pig to work out why (See [PR #50: fix Result Me bang (e.g. in TryFrom)](https://github.com/MusicalNinjaDad/try_v2/pull/50)). +- When working with references the compiler does not recognise `Foo::Ok(&!)` as an impossible variant and requires a match arm. +It is all too tempting to use `unreachable!()` here - but safer to rely on the compiler either via `Ok(&t) => match {}` (safest) or `Ok(t) => *t` (slightly less safe) + +## Complex cases + +I've already run into 3 cases where I was not able to derive `Try`. I find two of them to be fine - they are cases where I want direct control over the mechanics. + +### Struct with hidden inner + +In [proc_macro2_diagnostic](https://crates.io/crates/proc_macro2_diagnostic) I chose to hide the enum behind an opaque struct. Primarily, I wanted to keep the specifics of the stored type as an implementation detail and find a pub enum which cannot be deconstructed or directly constructed to be "nasty". Secondly, I wanted to keep the variants of the enum as an implementation detail, allowing me to adjust them later. + +This cost me some extra code but implementing `Try` etc. on a `struct` works perfectly fine. + +```rust +pub struct DiagnosticResult { + inner: DiagnosticResult_, +} + +impl std::ops::Try for DiagnosticResult { + type Output = T; + + type Residual = DiagnosticResult; + + fn from_output(output: Self::Output) -> Self { + Self { inner: Ok_(output) } + } +... +``` + +### ? with side-effects + +Let me start by offering an unrequested opinion: global state is inherently evil, hidden side-effects are inherently evil and usually rely on global state. + +And yet ... also in [proc_macro2_diagnostic](https://crates.io/crates/proc_macro2_diagnostic) I have `?` with side-effects :flushed:. Top-level compiler diagnostics (on nightly) are not all errors, a custom `Try` implementation allowed both fatal errors & non-fatal warnings: + +```rust +/// Result-like type which can represent a valid return value, an error or a warning accompanying +/// a valid return value. Warnings will be emitted upon `?`, allowing your code to continue with +/// the valid value. Errors will short-circuit upon `?` and be emitted upon final conversion to a +/// [proc_macro::TokenStream] +/// ... +pub struct DiagnosticResult { + inner: DiagnosticResult_, +} + +impl std::ops::Try for DiagnosticResult { + type Output = T; + + type Residual = DiagnosticResult; + + fn branch(self) -> std::ops::ControlFlow { + match self.inner { + Ok_(t) => std::ops::ControlFlow::Continue(t), + Warning(t, d) => { + d.emit(); + std::ops::ControlFlow::Continue(t) + } + Error(d) => std::ops::ControlFlow::Break(DiagnosticResult { inner: Error(d) }), + } + } +... +} +``` + +I'd consider this pattern both **inherently dangerous** and **invaluable in select cases**: + +- `LoggedResult` (near the top of my todo list) + + ```rust + /// Calling `?`: + /// - Ok(t) -> provides `t`; + /// - NonFatal(t, record) -> emits `record` to the logger & provides `t` + /// - Fatal(e) -> passes the error up the chain, without emitting anything. + pub enum LoggedResult { + Ok(T), + NonFatal(T, log::Record), + Fatal(E) + } + ``` + +- Async cases, e.g. for handling paged responses to a query: + + ```rust + /// Calling ?: + /// - LastPage(data) -> provides `data`; + /// - Page(data, next_page_uri) -> sends `next_page_uri` to page_handler channel & provides `data` + /// - Err(e) -> passes the error up the chain. + struct PagedResponse { + handler: async_channel::Sender, + payload: Payload + } + + enum Payload { + LastPage(T), + Page(T, http::uri::Uri), + Err(E), + } + ``` + +### Box vs Vec vs Option + +This one I find more annoying. The simple case is safe to derive for outputs `T` and `&'t T` but not for `Box`, `Vec` etc. The compiler currently does not identify anything other than a pure `!` (or `Infallible` etc.) as impossible when checking match arms. For the purpose of match-arm completeness we currently need to write: + +```rust +enum ValidatedBox { + ValidValue(Box), + InvalidValue, +} + +use ValidatedBox::{InvalidValue, ValidValue}; + +let x: ValidatedBox = InvalidValue; +let mut y = 0; + +y += match x { + InvalidValue => 1, + ValidValue(_) => unreachable!("no way to construct a Box"), +}; + +y += match x { + InvalidValue => 1, + ValidValue(b) => match *b {}, +}; + +assert_eq!(y, 2); +``` + +This requires either: + +- manually stating that code is unreachable, not something I want to do in derived code, or +- knowing the specifics of the wrapper used and how to convert it to the inner type (not possible in derived code). + +I can understand the troubles in differentiating: + +- `Box`, `Vec`, `Result` (all are verifiably impossible to construct) from +- `Option` (can be `None`)! + +This is something that would be a valuable, and non-trivial, improvement to the compiler to improve ergonomics as more people begin to use `Try` and therefore `!` + +## Other Std inconsistencies & niggles + +A few things I noticed in std niggled me slightly (in addition to the Box case above) + +### No clippy lint `must_use_try` + +`Result` & `ControlFlow` are marked `#[must_use]` for good reason. `Option` is not, but possibly should be. I've added a compiler warning in the derive macro if the type is not `#[must_use]` but this cannot be silenced (yet, todo) and is not _really_ the right approach. + +It would be a very valuable clippy lint to check that types which implement `Try` are labelled as `#[must_use]`. This would emit the warning when the user expects it - during linting - and can be silenced with an `#[allow(...)]`. + +### Poll - documentation + +While `Option` & [`Result`](https://doc.rust-lang.org/std/result/index.html#the-question-mark-operator-) nicely document using `?`, [Poll](https://doc.rust-lang.org/std/task/enum.Poll.html) does not. I'd consider it really valuable to understand why the two specific implementations were chosen and how they are intended to be used: + +```rust +impl FromResidual> for Poll>> +where + F: From, +``` + +and + +```rust +impl FromResidual> for Poll> +where + F: From, +``` + +### ControlFlow not ControlFlow + +`Result` and `Option` both lead with the generic for the Output type, ControlFlow does not. Given that the variants are ordered `Continue`, `Break` I find the alphabetical generics to be a regular source of "oops!" diff --git a/examples/BangMatching.rs b/examples/BangMatching.rs new file mode 100644 index 0000000..7b4c6a5 --- /dev/null +++ b/examples/BangMatching.rs @@ -0,0 +1,26 @@ +#![feature(never_type)] +#![allow(dead_code)] + +enum ValidatedBox { + ValidValue(Box), + InvalidValue, +} + +fn main() { + use ValidatedBox::{InvalidValue, ValidValue}; + + let x: ValidatedBox = InvalidValue; + let mut y = 0; + + y += match x { + InvalidValue => 1, + ValidValue(_) => unreachable!("no way to construct a Box"), + }; + + y += match x { + InvalidValue => 1, + ValidValue(b) => match *b {}, + }; + + assert_eq!(y, 2); +} diff --git a/examples/Nested_NoTry.rs b/examples/Nested_NoTry.rs new file mode 100644 index 0000000..3309689 --- /dev/null +++ b/examples/Nested_NoTry.rs @@ -0,0 +1,34 @@ +#![allow(dead_code, clippy::disallowed_names)] + +use std::{error::Error, io}; + +/// I _might_ have found somewhere that could contain duplicate info +/// Identifying duplicates _might_ have caused an error +/// And the answer _might_ be "no overlap" +type DuplicateData = Option, ValidErrors>>; + +enum ValidErrors { + Parsing(Box), + IO(Box), +} + +// There is no good way to unpack this for use, or pass it "up the chain" for handling without +// repeated let Some(Ok(Some()))-else-return directly in the code each time :( + +fn main() { + fn process(foo: DuplicateData) -> DuplicateData { + // This looks very much like: + // bar, err := foo + // if err != nil { + // return err + // } + let Some(Ok(Some(bar))) = foo else { + return foo; + }; + + let baz = bar + 1; + Some(Ok(Some(baz))) + } + let foo: DuplicateData = Some(Ok(Some(5))); + assert!(matches!(process(foo), Some(Ok(Some(6))))); +} diff --git a/examples/Nested_Try.rs b/examples/Nested_Try.rs new file mode 100644 index 0000000..f728927 --- /dev/null +++ b/examples/Nested_Try.rs @@ -0,0 +1,29 @@ +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] +#![allow(dead_code, clippy::disallowed_names)] + +use std::{error::Error, io}; + +use try_v2::Try; + +use DuplicateData::Duplicate; + +#[derive(Debug, Try)] +#[must_use] +enum DuplicateData { + Duplicate(T), + NoCandidate, + NoDuplicates, + ParsingError(Box), + IOError(Box), +} + +fn main() { + fn process(foo: DuplicateData) -> DuplicateData { + let baz = foo? + 1; + Duplicate(baz) + } + let foo: DuplicateData = Duplicate(5); + assert!(matches!(process(foo), Duplicate(6))); +} diff --git a/examples/TraitExt_NoTry.rs b/examples/TraitExt_NoTry.rs new file mode 100644 index 0000000..09f0af5 --- /dev/null +++ b/examples/TraitExt_NoTry.rs @@ -0,0 +1,37 @@ +#![allow(clippy::disallowed_names)] + +use log::info; + +/// A Counter which logs each adjustment +type Counter = Option; + +// While not so much extra work to double-specify everything as a trait & impl, it still +// wastes keystrokes and +trait NumberExt { + fn new() -> Self; + fn inc(self, n: N) -> Self; + // etc... +} + +impl NumberExt for Counter { + fn new() -> Self { + info!("new counter initialised"); + Some(0) + } + + // Not possible to impl std::ops::Add on a type alias, which leads to some people `impl Deref` + // to avoid peppering code with `.0` for a NewType or working around it in other ways, like this. + // + // Both cases force consideration of the implementation details in the code which uses Counter. + fn inc(self, n: i32) -> Self { + info!("adding {n} to counter"); + let n = self? + n; + info!("new value {n}"); + Some(n) + } +} + +fn main() { + let foo = Counter::new(); + assert!(matches!(foo.inc(2), Some(2))); +} diff --git a/examples/TraitExt_Try.rs b/examples/TraitExt_Try.rs new file mode 100644 index 0000000..d1387a6 --- /dev/null +++ b/examples/TraitExt_Try.rs @@ -0,0 +1,47 @@ +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] +#![allow(clippy::disallowed_names)] + +use log::info; +use std::{fmt::Display, ops::Add}; + +use try_v2::Try; + +use Counter::Count; + +#[derive(Debug, Try)] +#[must_use] +enum Counter { + Count(N), + Uninitialised, +} + +impl Counter { + fn new() -> Self { + info!("new counter initialised"); + Count(0) + } +} + +// More versatile implementation, better separating responsibilities: +// implementation details are owned here, type specifics at usage site. +impl Add for Counter +where + N: Add + Display, + M: Display, +{ + type Output = Self; + + fn add(self, rhs: M) -> Self::Output { + info!("adding {rhs} to counter"); + let n = self? + rhs; + info!("new value {n}"); + Self::Count(n) + } +} + +fn main() { + let foo = Counter::new(); + assert!(matches!(foo + 2, Count(n) if n ==2)); +} diff --git a/src/lib.rs b/src/lib.rs index 232a249..eb37932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ //! - the _first_ generic type must be the `Output` type (produced when not short-circuiting) //! - the output variant (does not short-circuit) must be the _first_ variant and store the output //! type as the _only unnamed_ field +//! - no other variant can store the Output type (TODO #72 add a nice error message) //! //! See the individual documentation for [Try], [Try_ConvertResult] and [Try_Iterator] for specifics //! on the generated code. @@ -523,7 +524,7 @@ fn impl_try_methods(input: TokenStream2) -> DiagnosticStream { /// } /// /// # fn main() { -/// let tests: Vec> = vec![Ok(1), TestsFailed, Ok(2), OtherError("something wierd"), Ok(3), Ok(4)]; +/// let tests: Vec> = vec![Ok(1), TestsFailed, Ok(2), OtherError("something weird"), Ok(3), Ok(4)]; /// /// let first_results: TestResult, &'static str> = tests.into_iter().collect(); /// assert!(matches!(first_results, TestsFailed)); diff --git a/tests/compilation/examples/wip_ShortCircuitT.rs b/tests/compilation/examples/wip_ShortCircuitT.rs new file mode 100644 index 0000000..dbb00b6 --- /dev/null +++ b/tests/compilation/examples/wip_ShortCircuitT.rs @@ -0,0 +1,17 @@ +//! WIP: Should fail with a better error message + +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] + +use try_v2::{Try, Try_ConvertResult}; + +#[derive(Debug, Try, Try_ConvertResult)] +#[must_use] +enum Exit { + Ok(T), + TestsFailed, + OtherError(T, E), +} + +fn main() {}