From 9bc440ce12ae2550d9d6bcb31de1302168c79833 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 14:23:20 +0000 Subject: [PATCH 01/29] braindump --- docs/hacking.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/hacking.md diff --git a/docs/hacking.md b/docs/hacking.md new file mode 100644 index 0000000..0cac784 --- /dev/null +++ b/docs/hacking.md @@ -0,0 +1,48 @@ +# Thoughts on `try_trait_v2` + +## Emotionally + +Don't like std can do stuff I can't +Don't like extra code to work around language constraints + +## Where using today + +### exit_safely + +### proc_macro2_diagnostic + +## The 2 + 1 + 1 traits + +## 2 more traits + +## Simple case + +3 std types + +### Flattening nested types + +### Flattening trait MyFunctionsExt + +### Boilerplate -> Derive + +- traits themselves +- all the nice functions that std lib have in common + +### Gotchas -> Derive + +- choice of residual +- interconversion with result, overlapping Into impls +- &! not infallible + +## Complex cases + +### struct with hidden inner + +### Box vs Vec vs Option + +### ? with sideeffects + +- global state inherently evil +- diagnosticresult +- loggedresult +- async & channels From 927ec9b0962c0220872b21face5cfaf70528d1d2 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 15:10:13 +0000 Subject: [PATCH 02/29] intro --- docs/hacking.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 0cac784..37060bc 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -1,23 +1,51 @@ # 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 -Don't like std can do stuff I can't -Don't like extra code to work around language constraints +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 contructs inside `Option`s & `Result`s + +## My related crates -## Where using today +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 -## The 2 + 1 + 1 traits +[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) + +## 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: + +- `trait Try` (`try_trait_v2`) +- `trait FromResidual` (`try_trait_v2`) +- `trait Residual` (`try_trait_v2_residual`) +- `type !` (`never_type`) +- `fn try_collect()` (`iterator_try_collect`) + +### 2 more experimental features -## 2 more traits +As wierd as it may be from the naming `try_blocks`, `try_blocks_heterogeneous` are more separate from a usage point of view. ## Simple case -3 std types +3 std types + Poll? ### Flattening nested types From a901e4d118add66bec78a4c070f10b5a68b49bcf Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 16:15:19 +0000 Subject: [PATCH 03/29] flattening example --- docs/hacking.md | 75 +++++++++++++++++++++++++++++++++++++++++++++- examples/nested.rs | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 examples/nested.rs diff --git a/docs/hacking.md b/docs/hacking.md index 37060bc..1715a1a 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -45,14 +45,81 @@ As wierd as it may be from the naming `try_blocks`, `try_blocks_heterogeneous` a ## Simple case -3 std types + Poll? +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_ know whether I have an answer at all +/// Getting the answer _might_ have caused an error +/// And the answer _might_ be "nope" +type MaybeMaybe = Option, ValidErrors>>; + +/// If it's an error, it matters what sort ... +enum ValidErrors { + /// Parsing something failed + Parsing(Box), + /// Getting some data failed + IO(Box), +} + +// There is no good way to unpack this for use, or pass it "up the chain" for handling without +// repeated let-if-let-else-return directly in the code each time :( + +fn main() { + fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + let bar = if let Some(Ok(Some(value))) = foo { + value + } else { + return foo; + }; + let baz = bar + 1; + Some(Ok(Some(baz))) + } + let foo: MaybeMaybe = Some(Ok(Some(5))); + assert!(matches!(increment_foo(foo), Some(Ok(Some(6))))); +} +``` + +Wouldn't it be nicer to be able to have everthing 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 MaybeMaybe::{Ok}; + +#[derive(Debug, Try)] +#[must_use] +enum MaybeMaybe { + Ok(T), + NoAnswer, + NoValue, + ParsingError(Box), + IOError(Box), +} + +fn main() { + fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + let baz = foo? + 1; + Ok(baz) + } + let foo: MaybeMaybe = Ok(5); + assert!(matches!(increment_foo(foo), Ok(6))); +} +``` + ### Flattening trait MyFunctionsExt + + ### Boilerplate -> 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 + - traits themselves - all the nice functions that std lib have in common @@ -62,6 +129,12 @@ As wierd as it may be from the naming `try_blocks`, `try_blocks_heterogeneous` a - interconversion with result, overlapping Into impls - &! not infallible +### Std inconsistencies & niggles + +#### Poll - documentation + +#### ControlFlow B, C not C, B + ## Complex cases ### struct with hidden inner diff --git a/examples/nested.rs b/examples/nested.rs new file mode 100644 index 0000000..d41cb76 --- /dev/null +++ b/examples/nested.rs @@ -0,0 +1,60 @@ +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] +#![allow(dead_code, clippy::disallowed_names)] + +use std::{error::Error, io}; + +/// I _might_ know whether I have an answer at all +/// Getting the answer _might_ have caused an error +/// And the answer _might_ be "nope" +type MaybeMaybe = 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-if-let-else-return directly in the code each time :( + +fn main() { + fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + let bar = if let Some(Ok(Some(value))) = foo { + value + } else { + return foo; + }; + let baz = bar + 1; + Some(Ok(Some(baz))) + } + let foo: MaybeMaybe = Some(Ok(Some(5))); + assert!(matches!(increment_foo(foo), Some(Ok(Some(6))))); +} + +mod with_try { + use std::{error::Error, io}; + + use try_v2::Try; + + use MaybeMaybe::Ok; + + #[derive(Debug, Try)] + #[must_use] + enum MaybeMaybe { + Ok(T), + NoAnswer, + NoValue, + ParsingError(Box), + IOError(Box), + } + + fn main() { + fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + let baz = foo? + 1; + Ok(baz) + } + let foo: MaybeMaybe = Ok(5); + assert!(matches!(increment_foo(foo), Ok(6))); + } +} From 1b2af59993394aeed0b0c403c040e5380f0e1b3f Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 16:50:11 +0000 Subject: [PATCH 04/29] ext trait example (needs unwrap) --- examples/ext.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 examples/ext.rs diff --git a/examples/ext.rs b/examples/ext.rs new file mode 100644 index 0000000..fbdfb64 --- /dev/null +++ b/examples/ext.rs @@ -0,0 +1,72 @@ +#![feature(never_type)] +#![feature(try_trait_v2)] +#![feature(try_trait_v2_residual)] +#![allow(dead_code, clippy::disallowed_names)] + +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) -> Self; + // etc... +} + +impl NumberExt for Counter { + fn new() -> Self { + Some(0) + } + + fn inc(self) -> Self { + Some(self? + 1) + } +} + +fn main() { + let foo = Counter::new(); + assert_eq!(foo.inc().unwrap(), 1); +} + +mod with_try { + use std::ops::Add; + + use try_v2::Try; + + use Counter::Count; + + #[derive(Debug, Try)] + #[must_use] + enum Counter { + Count(N), + Uninitialised, + } + + impl Counter { + fn new() -> Self { + Count(0) + } + + fn inc(self) -> Self { + self + 1 + } + } + + /// Not possible 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. + impl Add for Counter + where + N: Add, + { + type Output = Self; + + fn add(self, rhs: M) -> Self::Output { + Self::Count(self? + rhs) + } + } + + fn main() { + let foo = Counter::new(); + assert_eq!(foo.inc().unwrap(), 1); + } +} From 244fe06a7d8e3afb1feb56edb532d5121ff4063f Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 17:29:58 +0000 Subject: [PATCH 05/29] ext example more real world - logging --- Cargo.toml | 1 + examples/ext.rs | 51 +++++++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9bcdf64..1a4f576 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/examples/ext.rs b/examples/ext.rs index fbdfb64..a8d4271 100644 --- a/examples/ext.rs +++ b/examples/ext.rs @@ -3,33 +3,45 @@ #![feature(try_trait_v2_residual)] #![allow(dead_code, clippy::disallowed_names)] -type Counter = Option; +use log::info; -/// While not so much extra work to double-specify everything as a trait & impl, it still -/// wastes keystrokes and -trait NumberExt { +/// 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) -> Self; + fn inc(self, n: N) -> Self; // etc... } -impl NumberExt for Counter { +impl NumberExt for Counter { fn new() -> Self { + info!("new counter initialised"); Some(0) } - fn inc(self) -> Self { - Some(self? + 1) + // 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_eq!(foo.inc().unwrap(), 1); + assert_eq!(foo.inc(2).unwrap(), 2); } mod with_try { - use std::ops::Add; + use std::{fmt::Display, ops::Add}; + use log::info; use try_v2::Try; @@ -44,29 +56,30 @@ mod with_try { impl Counter { fn new() -> Self { + info!("new counter initialised"); Count(0) } - - fn inc(self) -> Self { - self + 1 - } } - /// Not possible 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. + // More versatile implementation, better separating responsibilities: + // implementation details are owned here, type specifics at usage site. impl Add for Counter where - N: Add, + N: Add + Display, + M: Display, { type Output = Self; fn add(self, rhs: M) -> Self::Output { - Self::Count(self? + rhs) + info!("adding {rhs} to counter"); + let n = self? + rhs; + info!("new value {n}"); + Self::Count(n) } } fn main() { let foo = Counter::new(); - assert_eq!(foo.inc().unwrap(), 1); + assert!(matches!(foo + 2, Count(n) if n ==2)); } } From 85f20882d9dd763db5ca99ae72402bbcae7891c8 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 14 Apr 2026 17:59:36 +0000 Subject: [PATCH 06/29] nested example more real-world --- docs/hacking.md | 41 ++++++++++++++++++----------------------- examples/ext.rs | 2 +- examples/nested.rs | 32 ++++++++++++++++---------------- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 1715a1a..6716b28 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -52,16 +52,13 @@ Std contains 3 types which impl Try for all cases: `Result`, `Option` & ```rust use std::{error::Error, io}; -/// I _might_ know whether I have an answer at all -/// Getting the answer _might_ have caused an error -/// And the answer _might_ be "nope" -type MaybeMaybe = Option, ValidErrors>>; +/// 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>>; -/// If it's an error, it matters what sort ... enum ValidErrors { - /// Parsing something failed Parsing(Box), - /// Getting some data failed IO(Box), } @@ -69,7 +66,7 @@ enum ValidErrors { // repeated let-if-let-else-return directly in the code each time :( fn main() { - fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + fn process(foo: DuplicateData) -> DuplicateData { let bar = if let Some(Ok(Some(value))) = foo { value } else { @@ -78,8 +75,8 @@ fn main() { let baz = bar + 1; Some(Ok(Some(baz))) } - let foo: MaybeMaybe = Some(Ok(Some(5))); - assert!(matches!(increment_foo(foo), Some(Ok(Some(6))))); + let foo: DuplicateData = Some(Ok(Some(5))); + assert!(matches!(process(foo), Some(Ok(Some(6))))); } ``` @@ -90,35 +87,33 @@ use std::{error::Error, io}; use try_v2::Try; -use MaybeMaybe::{Ok}; +use DuplicateData::Duplicate; #[derive(Debug, Try)] #[must_use] -enum MaybeMaybe { - Ok(T), - NoAnswer, - NoValue, +enum DuplicateData { + Duplicate(T), + NoCandidate, + NoDuplicates, ParsingError(Box), IOError(Box), } fn main() { - fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + fn process(foo: DuplicateData) -> DuplicateData { let baz = foo? + 1; - Ok(baz) + Duplicate(baz) } - let foo: MaybeMaybe = Ok(5); - assert!(matches!(increment_foo(foo), Ok(6))); + let foo: DuplicateData = Duplicate(5); + assert!(matches!(process(foo), Duplicate(6))); } ``` ### Flattening trait MyFunctionsExt - - ### Boilerplate -> 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 +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 - traits themselves - all the nice functions that std lib have in common @@ -146,4 +141,4 @@ While the above is really easy to create, use and reason about, manually impleme - global state inherently evil - diagnosticresult - loggedresult -- async & channels +- async & channels (LastPage, Page, Err) diff --git a/examples/ext.rs b/examples/ext.rs index a8d4271..f559a82 100644 --- a/examples/ext.rs +++ b/examples/ext.rs @@ -40,8 +40,8 @@ fn main() { } mod with_try { - use std::{fmt::Display, ops::Add}; use log::info; + use std::{fmt::Display, ops::Add}; use try_v2::Try; diff --git a/examples/nested.rs b/examples/nested.rs index d41cb76..7759b28 100644 --- a/examples/nested.rs +++ b/examples/nested.rs @@ -5,10 +5,10 @@ use std::{error::Error, io}; -/// I _might_ know whether I have an answer at all -/// Getting the answer _might_ have caused an error -/// And the answer _might_ be "nope" -type MaybeMaybe = Option, ValidErrors>>; +/// 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), @@ -19,7 +19,7 @@ enum ValidErrors { // repeated let-if-let-else-return directly in the code each time :( fn main() { - fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + fn process(foo: DuplicateData) -> DuplicateData { let bar = if let Some(Ok(Some(value))) = foo { value } else { @@ -28,8 +28,8 @@ fn main() { let baz = bar + 1; Some(Ok(Some(baz))) } - let foo: MaybeMaybe = Some(Ok(Some(5))); - assert!(matches!(increment_foo(foo), Some(Ok(Some(6))))); + let foo: DuplicateData = Some(Ok(Some(5))); + assert!(matches!(process(foo), Some(Ok(Some(6))))); } mod with_try { @@ -37,24 +37,24 @@ mod with_try { use try_v2::Try; - use MaybeMaybe::Ok; + use DuplicateData::Duplicate; #[derive(Debug, Try)] #[must_use] - enum MaybeMaybe { - Ok(T), - NoAnswer, - NoValue, + enum DuplicateData { + Duplicate(T), + NoCandidate, + NoDuplicates, ParsingError(Box), IOError(Box), } fn main() { - fn increment_foo(foo: MaybeMaybe) -> MaybeMaybe { + fn process(foo: DuplicateData) -> DuplicateData { let baz = foo? + 1; - Ok(baz) + Duplicate(baz) } - let foo: MaybeMaybe = Ok(5); - assert!(matches!(increment_foo(foo), Ok(6))); + let foo: DuplicateData = Duplicate(5); + assert!(matches!(process(foo), Duplicate(6))); } } From 6d8e9a69e2165ed1b924efb13126543dea7e941b Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 06:41:27 +0000 Subject: [PATCH 07/29] add TraitExt example --- docs/hacking.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ examples/ext.rs | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/docs/hacking.md b/docs/hacking.md index 6716b28..3092f0d 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -111,6 +111,97 @@ fn main() { ### 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 -> 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 diff --git a/examples/ext.rs b/examples/ext.rs index f559a82..2b905a3 100644 --- a/examples/ext.rs +++ b/examples/ext.rs @@ -36,7 +36,7 @@ impl NumberExt for Counter { fn main() { let foo = Counter::new(); - assert_eq!(foo.inc(2).unwrap(), 2); + assert!(matches!(foo.inc(2), Some(2))); } mod with_try { From dd773044071395608911f3c0d1af56fec71d73c0 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 06:46:09 +0000 Subject: [PATCH 08/29] separate examples so Try versions are also tested --- examples/{nested.rs => Nested_NoTry.rs} | 30 --------- examples/Nested_Try.rs | 29 +++++++++ examples/TraitExt_NoTry.rs | 37 +++++++++++ examples/TraitExt_Try.rs | 47 ++++++++++++++ examples/ext.rs | 85 ------------------------- 5 files changed, 113 insertions(+), 115 deletions(-) rename examples/{nested.rs => Nested_NoTry.rs} (58%) create mode 100644 examples/Nested_Try.rs create mode 100644 examples/TraitExt_NoTry.rs create mode 100644 examples/TraitExt_Try.rs delete mode 100644 examples/ext.rs diff --git a/examples/nested.rs b/examples/Nested_NoTry.rs similarity index 58% rename from examples/nested.rs rename to examples/Nested_NoTry.rs index 7759b28..6fcc042 100644 --- a/examples/nested.rs +++ b/examples/Nested_NoTry.rs @@ -1,6 +1,3 @@ -#![feature(never_type)] -#![feature(try_trait_v2)] -#![feature(try_trait_v2_residual)] #![allow(dead_code, clippy::disallowed_names)] use std::{error::Error, io}; @@ -31,30 +28,3 @@ fn main() { let foo: DuplicateData = Some(Ok(Some(5))); assert!(matches!(process(foo), Some(Ok(Some(6))))); } - -mod with_try { - 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/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/examples/ext.rs b/examples/ext.rs deleted file mode 100644 index 2b905a3..0000000 --- a/examples/ext.rs +++ /dev/null @@ -1,85 +0,0 @@ -#![feature(never_type)] -#![feature(try_trait_v2)] -#![feature(try_trait_v2_residual)] -#![allow(dead_code, 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))); -} - -mod with_try { - 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)); - } -} From 1f6de4d8a30bc879aa33b1d589522f34299cbbb1 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 07:11:08 +0000 Subject: [PATCH 09/29] add todo #72 --- README.md | 1 + src/lib.rs | 1 + tests/compilation/examples/wip_ShortCircuitT.rs | 15 +++++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 tests/compilation/examples/wip_ShortCircuitT.rs 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/src/lib.rs b/src/lib.rs index 232a249..c0b94ba 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. diff --git a/tests/compilation/examples/wip_ShortCircuitT.rs b/tests/compilation/examples/wip_ShortCircuitT.rs new file mode 100644 index 0000000..4e03ab4 --- /dev/null +++ b/tests/compilation/examples/wip_ShortCircuitT.rs @@ -0,0 +1,15 @@ +#![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() {} From 65fd49c9e6784acea344d36a073e0030e92552af Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 07:45:11 +0000 Subject: [PATCH 10/29] start explaining the derive macros --- docs/hacking.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 3092f0d..58e3fee 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -202,9 +202,134 @@ fn main() { } ``` -### Boilerplate -> Derive +## 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 +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). + +### 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 (TODO #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. + +### Derivable code + +For the following case + +```rust +#[derive(Try, Try_ConvertResult)] +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 bidirection `FromResidual` with `Result` + +will generate + +```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 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 edge. + +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)); +``` - traits themselves - all the nice functions that std lib have in common @@ -214,6 +339,7 @@ While the above is really easy to create, use and reason about, manually impleme - choice of residual - interconversion with result, overlapping Into impls - &! not infallible +- Interconversion with Option ### Std inconsistencies & niggles From 738400aa622f1c712361068cffbe3a58196ef235 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 08:13:21 +0000 Subject: [PATCH 11/29] finish explaining derive macros --- docs/hacking.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 58e3fee..990e090 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -206,6 +206,8 @@ fn main() { 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: @@ -331,8 +333,39 @@ assert!(matches!(even_string(2), Eightball::Yes(s) if s == "2")); assert!(matches!(even_string(1), Eightball::No)); ``` -- traits themselves -- all the nice functions that std lib have in common +#### 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 wierd"), 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 sematically 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. ### Gotchas -> Derive From 31fed2d5e98cd6f49fe66913f2c5d0d9cd678b4d Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 08:31:14 +0000 Subject: [PATCH 12/29] std niggles --- docs/hacking.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 990e090..8ba18ee 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -367,18 +367,38 @@ assert_eq!(result, Some(5)); 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. -### Gotchas -> Derive +### Gotcha!s -> Derive -- choice of residual -- interconversion with result, overlapping Into impls -- &! not infallible -- Interconversion with Option +There are a few "gotcha!s" with even the simple implementation which can be easily avoided. + +- 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) ### Std inconsistencies & niggles +A few things I noticed in std niggled me slightly + #### Poll - documentation -#### ControlFlow B, C not C, B +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!" ## Complex cases From 156bd78576737a12bd8b5e9b24467408dce19252 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 08:57:19 +0000 Subject: [PATCH 13/29] missing clippy lint --- docs/hacking.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/hacking.md b/docs/hacking.md index 8ba18ee..d6854fb 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -378,6 +378,12 @@ There are a few "gotcha!s" with even the simple implementation which can be easi A few things I noticed in std niggled me slightly +#### 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: From ce226111cebcca86f5330f199285e594b58b6dbd Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:05:24 +0000 Subject: [PATCH 14/29] first two (validly) complex cases --- docs/hacking.md | 65 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index d6854fb..cc91ad7 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -408,13 +408,64 @@ where ## Complex cases -### struct with hidden inner +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. -### Box vs Vec vs Option +### 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. + +### ? 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. -### ? with sideeffects +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_, +} +``` -- global state inherently evil -- diagnosticresult -- loggedresult -- async & channels (LastPage, Page, Err) +I'd consider the 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, not so easily lib-ified, 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 From acc3c4557622a1ea1da2a20c51666296fc2f5da4 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:23:48 +0000 Subject: [PATCH 15/29] unmatchable bangs --- docs/hacking.md | 30 ++++++++++++++++++++++++++++++ examples/BangMatching.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 examples/BangMatching.rs diff --git a/docs/hacking.md b/docs/hacking.md index cc91ad7..ea38e4d 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -469,3 +469,33 @@ I'd consider the pattern both inherently dangerous and invaluable in select case ``` ### 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); +``` + +which 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 `!` 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); +} From bb63aa76bfd527c0e92ab8f31bdb38534da168fe Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:27:54 +0000 Subject: [PATCH 16/29] annotated feature relationships --- docs/hacking.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index ea38e4d..e309c88 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -31,13 +31,13 @@ After working with the trait in various use cases, taking it apart to try (!) an ## The 3 traits + 1 type + 1 function -When talking about `Try` below, I will usually consider the following traits in one package: +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`) -- `trait Residual` (`try_trait_v2_residual`) -- `type !` (`never_type`) -- `fn try_collect()` (`iterator_try_collect`) +- `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 @@ -496,6 +496,14 @@ y += match x { assert_eq!(y, 2); ``` -which 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). +This requires either: -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 `!` +- 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 `!` From d81c27af50f3700cc9978587fa72f4ed36271cc2 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:29:39 +0000 Subject: [PATCH 17/29] proof-read --- docs/hacking.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index e309c88..2e8ee43 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -25,9 +25,9 @@ To date I've thought about using the trait multiple times but always found I wou [MusicalNinjaDad/try_v2](https://github.com/MusicalNinjaDad/try_v2) provides a set of derive macros to make `Try` more accessible. (See below for details) -## Criticism: complexity +## (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. +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 From 7554f91ed9d728e60ec1690d99734845a09847a7 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:35:26 +0000 Subject: [PATCH 18/29] remove excessive let if let else --- docs/hacking.md | 12 ++++++++---- examples/Nested_NoTry.rs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 2e8ee43..bb1e011 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -63,15 +63,19 @@ enum ValidErrors { } // There is no good way to unpack this for use, or pass it "up the chain" for handling without -// repeated let-if-let-else-return directly in the code each time :( +// repeated let Some(Ok(Some()))-else-return directly in the code each time :( fn main() { fn process(foo: DuplicateData) -> DuplicateData { - let bar = if let Some(Ok(Some(value))) = foo { - value - } else { + // 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))) } diff --git a/examples/Nested_NoTry.rs b/examples/Nested_NoTry.rs index 6fcc042..1098d2f 100644 --- a/examples/Nested_NoTry.rs +++ b/examples/Nested_NoTry.rs @@ -13,15 +13,19 @@ enum ValidErrors { } // There is no good way to unpack this for use, or pass it "up the chain" for handling without -// repeated let-if-let-else-return directly in the code each time :( +// repeated let Some(Ok(Some()))-else-return directly in the code each time :( fn main() { fn process(foo: DuplicateData) -> DuplicateData { - let bar = if let Some(Ok(Some(value))) = foo { - value - } else { + // 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))) } From da27dc369fa33ec941cda1f45de7199c8f0a38bf Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:39:08 +0000 Subject: [PATCH 19/29] proof-read --- docs/hacking.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/hacking.md b/docs/hacking.md index bb1e011..09380b8 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -84,7 +84,9 @@ fn main() { } ``` -Wouldn't it be nicer to be able to have everthing 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. +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}; From e148182d3684e9c232c632673b32de0b880ffa56 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:56:07 +0000 Subject: [PATCH 20/29] proof-read --- docs/hacking.md | 2 +- examples/Nested_NoTry.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 09380b8..19382ca 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -222,7 +222,7 @@ The simple case described above is derivable (as in the examples) and is probabl - 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) +- 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. diff --git a/examples/Nested_NoTry.rs b/examples/Nested_NoTry.rs index 1098d2f..3309689 100644 --- a/examples/Nested_NoTry.rs +++ b/examples/Nested_NoTry.rs @@ -25,7 +25,7 @@ fn main() { let Some(Ok(Some(bar))) = foo else { return foo; }; - + let baz = bar + 1; Some(Ok(Some(baz))) } From b9f07dabf577ffc7a9e70f139f90e6b75693cf12 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:56:49 +0000 Subject: [PATCH 21/29] remove l4 headings --- docs/hacking.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 19382ca..e5daa65 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -224,11 +224,11 @@ The simple case described above is derivable (as in the examples) and is probabl - 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. +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. -### Derivable code +### Derive Example -For the following case +For the following case (based upon the usage in [pt](https://github.com/MusicalNinjaDad/pt)) ```rust #[derive(Try, Try_ConvertResult)] @@ -239,7 +239,7 @@ enum TestResult { } ``` -#### Macro `Try`: derives `Try`, `FromResidual` and `Residual` +### Macro `Try`: derives `Try`, `FromResidual` and `Residual` will result in code of the shape: @@ -271,7 +271,7 @@ impl Residual for TestResult { } ``` -#### Macro `Try_ConvertResult`: derives bidirection `FromResidual` with `Result` +### Macro `Try_ConvertResult`: derives bidirectional `FromResidual` with `Result` will generate @@ -339,7 +339,7 @@ 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` +### 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: @@ -361,7 +361,7 @@ let result = test.into_iter().next(); assert_eq!(result, Some(5)); ``` -#### Macro `Try_Methods` (WIP): derives `unwrap()` +### Macro `Try_Methods` (WIP): derives `unwrap()` `Option` & `Result` have a large set of sematically overlapping ergonomic methods for: From b3a238dc1552516a18615000caa24f71375eb967 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:58:10 +0000 Subject: [PATCH 22/29] add other derives to example --- docs/hacking.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index e5daa65..b6b87f3 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -231,7 +231,7 @@ While technically, the generic ordering requirement could be relaxed with slight For the following case (based upon the usage in [pt](https://github.com/MusicalNinjaDad/pt)) ```rust -#[derive(Try, Try_ConvertResult)] +#[derive(Try, Try_ConvertResult, Try_Iterator, Try_Methods)] enum TestResult { Ok(T), TestsFailed, @@ -273,7 +273,7 @@ impl Residual for TestResult { ### Macro `Try_ConvertResult`: derives bidirectional `FromResidual` with `Result` -will generate +adds ```rust From 11186cbcecf2b79b1090097b44e6ed104946b6d2 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 09:59:09 +0000 Subject: [PATCH 23/29] header for why into Foo --- docs/hacking.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/hacking.md b/docs/hacking.md index b6b87f3..51936df 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -294,7 +294,9 @@ where ... which calls Result::Err(residual.into()) ... ``` -Why require `From/Into Foo` and not `Foo<_>`? 2 reasons: +#### 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 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 edge. From 24a1e6fc575904260e70fca1fe0b678d9f46840a Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:02:46 +0000 Subject: [PATCH 24/29] proof read dervie section --- docs/hacking.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 51936df..865369e 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -299,7 +299,7 @@ where 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 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 edge. +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` @@ -377,10 +377,12 @@ Current task in progress is to derive equivalent methods named according to the ### Gotcha!s -> Derive -There are a few "gotcha!s" with even the simple implementation which can be easily avoided. +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) +- 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) ### Std inconsistencies & niggles From 43dc0bac9161db28cc481329c410744a9138bfe0 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:04:18 +0000 Subject: [PATCH 25/29] linebreaks in bullets --- docs/hacking.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index 865369e..a6ccb7f 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -379,10 +379,10 @@ Current task in progress is to derive equivalent methods named according to the 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) +- 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) ### Std inconsistencies & niggles From 47e21f9c94c2e21639f6ca51a7abe053233dc792 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:13:09 +0000 Subject: [PATCH 26/29] extend example code in complex cases --- docs/hacking.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index a6ccb7f..b2e4e98 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -388,7 +388,7 @@ It is all too tempting to use `unreachable!()` here - but safer to rely on the c A few things I noticed in std niggled me slightly -#### No clippy lint must_use_try +#### 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. @@ -426,6 +426,22 @@ In [proc_macro2_diagnostic](https://crates.io/crates/proc_macro2_diagnostic) I c 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. @@ -441,9 +457,27 @@ And yet ... also in [proc_macro2_diagnostic](https://crates.io/crates/proc_macro 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 the pattern both inherently dangerous and invaluable in select cases: +I'd consider this pattern both **inherently dangerous** and **invaluable in select cases**: - `LoggedResult` (near the top of my todo list) @@ -459,7 +493,7 @@ I'd consider the pattern both inherently dangerous and invaluable in select case } ``` -- Async cases, not so easily lib-ified, e.g. for handling paged responses to a query: +- Async cases, e.g. for handling paged responses to a query: ```rust /// Calling ?: From 0b4d1318d2f28980db03a5d9977fea03b0221f70 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:14:32 +0000 Subject: [PATCH 27/29] move std niggles to own, final section --- docs/hacking.md | 64 ++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index b2e4e98..dc002ab 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -384,38 +384,6 @@ This one bit me in the ass when using my own macros - while it may feel slightly - 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) -### Std inconsistencies & niggles - -A few things I noticed in std niggled me slightly - -#### 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!" - ## 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. @@ -551,3 +519,35 @@ I can understand the troubles in differentiating: - `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!" From 040a4f415c7bad68d3ebb14bc36dd10fb6f046d5 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:22:54 +0000 Subject: [PATCH 28/29] typos --- docs/hacking.md | 10 +++++----- src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index dc002ab..315c7ea 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -7,7 +7,7 @@ Let me start off by saying, I _love_ `trait Try` and really hope to see it accep 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 contructs inside `Option`s & `Result`s +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 @@ -41,7 +41,7 @@ When talking about `Try` below, I will usually consider the following traits in ### 2 more experimental features -As wierd as it may be from the naming `try_blocks`, `try_blocks_heterogeneous` are more separate from a usage point of view. +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 @@ -222,7 +222,7 @@ The simple case described above is derivable (as in the examples) and is probabl - 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) +- 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. @@ -346,7 +346,7 @@ assert!(matches!(even_string(1), Eightball::No)); 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 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)); @@ -365,7 +365,7 @@ assert_eq!(result, Some(5)); ### Macro `Try_Methods` (WIP): derives `unwrap()` -`Option` & `Result` have a large set of sematically overlapping ergonomic methods for: +`Option` & `Result` have a large set of semantically overlapping ergonomic methods for: - Querying the variant - Adapters for working with references (only `Option`) diff --git a/src/lib.rs b/src/lib.rs index c0b94ba..eb37932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -524,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)); From 5907951a8855a2df3be945ab82eadc8e6575727c Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 15 Apr 2026 10:23:08 +0000 Subject: [PATCH 29/29] clarify intent of wip_ShortCircuitT test --- tests/compilation/examples/wip_ShortCircuitT.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/compilation/examples/wip_ShortCircuitT.rs b/tests/compilation/examples/wip_ShortCircuitT.rs index 4e03ab4..dbb00b6 100644 --- a/tests/compilation/examples/wip_ShortCircuitT.rs +++ b/tests/compilation/examples/wip_ShortCircuitT.rs @@ -1,3 +1,5 @@ +//! WIP: Should fail with a better error message + #![feature(never_type)] #![feature(try_trait_v2)] #![feature(try_trait_v2_residual)]