Please - give me cases that break this generic implementation of map() etc

Hey folks,

Just crawling out of a rabbit-hole after spending a few hours crafting Try-related trait bounds and generic implementations of map, zip, transpose etc for any TryType

I'd be really grateful for ideas of cases where this is broken.

Thanks!

I'm a bit at a loss here, to be honest. I've had a look at your crate and compared it with the standard library. Everything you've implemented is already available in the standard library. What's more, the standard library explains how to use all these features. Unfortunately, your crate doesn't do that. So what is the purpose of your crate?

Great, eye-opening question.Thanks! (I'm not an AI, I really did learn to start a response like that when I grew up)

Right now Option & Result are the only[1] cases where you can apply ?. There is an experimental feature on nightly that makes the underlying traits (Try etc.) available for anyone to implement on their own types. When you do that 2 things happen:

  1. You write a load of boilerplate & risk a couple of footguns to implement the basic case (the crate provides a Derive macro for that)
  2. You end up with a type that is missing fundamental ergonomic functions, which you only notice once some time later when you (and potentially others) are already using it and get stuck. The trait above aims to resolve this.

That was the aim - and is good confirmation for me. (Option & Result both hand-roll these functions as direct methods) although I think there are a couple which are missing on one or the other (e.g. Result doesn't have .zip() IIRC)

I hear you, documentation needs examples. Thanks - I would probably have skipped them in most cases for just a little more text[2].


  1. Poll is a whole other discussion, so simplifying it away for this answer. ↩︎

  2. My personal dev cycle usually starts with "Note to self" doc comments and ends with "full code & API review by writing docs, examples and polishing the code as I go" - I've not done that final step on this one as I'm not at the point I feel comfortable enough to say "time to polish this" ↩︎

I think what this is really making me think is about this old thread:

Lots of those implementations you have are easy one-liners with try, like map is just try { f(x?) }, flatten is try { a?? }, and zip is try { (a?, b?) }.

The places that aren't are things like map_or. So I'd love to have a generalized construct covering those cases too. Like if unwrap_or_else is coalesce![ a?, 0 ] and map_or_else is coalesce![ f1(a?), f2() ] (or something).

yeah, I also prefer try blocks or bare ?. I only missed the functions when I got into the middle of a functional chain, and was amazed just how simple some of them are when I started implementing them. And flatten only needs to be there to allow for custom implementations if needed[1].

If spanish keyboards were standard we could have ¿ - so unwrap_or_else would be ¿a?,0¿ ... now I've written that ... maybe not ... but I do like the coalesce idea. Maybe an "inverted try" block:

yrt {
  a?; // desugars to return on Continue; continue on Break(Residual)
  0
}

which could even allow

yrt {
  let err = a?; // desugars to return on Continue; continue on Break(Residual)
  len(format!("{err}"))
}

although it probably adds too much cognitive complexity to remember that ? does something different, so we'd still need an alternative operator, and that seems to be where the old thread ran aground.


  1. e.g. ... Poll :beer_mug: ↩︎

I've also recently added and, or etc. and got stuck deciding whether to allow
ok(5).or(None) == Some(5) or forbid Result<_,E>::Ok(5).or(Result<_,F>::Ok(6))

Any thoughts on how to add a restriction that forbids Result.or(Option) but allows Result<_,E>.or(Result<_,F>)[1]:

/// This will convert types! - so `Ok(5).or(None) = Some(5)`
    fn or<Y>(self, other: Y) -> Y
    where
        Y: Try<Output = Self::Output>,
    {
        match self.branch() {
            ControlFlow::Continue(v) => Try::from_output(v),
            ControlFlow::Break(_) => other,
        }
    }

  1. They don't share a residual Result<!,E>vs Result<!,F>, or a canonical TryType :cry: ↩︎

flip(): Result<T,E> -> Result<E,T> would be simpler in that regard.

but wouldn't work for Option, any TryType that has more than one residual variant, or where the residual and output variants have differently structured fields (which is why Option fails)

I like the “inverted try” idea. And how about try_not? Would make for a good joke, too: “try, or try_not. There is no do.”

I’m not sure that there’s a good replacement for ?, but maybe the “not” in try_not is a good enough hint?

TBH, I think that a coalesce like this is better not just being a subtle operator the way ? is.

The point of ? is that you can mostly ignore it to read what the function does, and there's likely to be a whole bunch even in a single expression.

When instead you're doing the "try this, then that, then some other thing", I think expressing that in a more match-like structure that highlights the different cases is actually better.

foo()¿.bar()¿.quz()¿ flowing data from one to the next makes much less sense for coalescing things than it does for bubbling with ?.