[Pre-RFC] `Result::void()` and `Option::void()`

Iterator and Future are also very cared about monads, the latter has a whole language syntax dedicated to it, and adding some syntax to simplify the former has been widely discussed.

4 Likes

C void corresponds to Rust (), so that was my first association. :wink: The never type does not really fit. (But sadly, C void is often described as "containing no value", which of course is wrong as we can easily see in Rust where we have the value () inhabiting the type ().)

2 Likes

The naming is purely historical: I called it void here because that's the symbol name in Haskell and Scala (at least).

Sounds like you want fehler?

Thanks for the suggestion, but the main point of void is to "drop to ()", which is useful in a number of places. All the discussion involving error handling has to do with a void call being made as the last line of function.

That's what a semicolon is for. E.g. with fehler, your original example becomes:

#[throws(Error)]
fn foo() {
    let a = bar()?;
    let b = baz()?;
    work(a, b)?;
}
1 Like

Sorry, please reread the original posting and comments.

1 Like

Please don't assume that people who disagree with you haven't read what you've written.

I've read your entire original post, and I don't see the use case for .void(). For Option it never seems useful (Option<()> is rarely a useful type) and for Result it seems like a niche helper that might be helpful for the occasional closure argument but not sufficiently common to put in the standard library.

10 Likes

For what it's worth, here are your other examples rewritten to use fehler. Note that fehler has the same advantages as try blocks but without the disadvantage of additional nesting.

#[throws(Error)]
pub(crate) fn search(path: &Path, term: String) {
    let (search, args) = misc::searcher();
    Command::new(search)
        .args(args)
        .arg(term)
        .arg(path)
        .status()?;
}
/// Open a given URL in a browser.
#[throws(std::io::Error)]
pub(crate) fn open(url: &str) {
    webbrowser::open(url)?;
}
#[throws(&str)]
fn clone(p: &String) {
    let pkg = p.as_str();
    aura!(fll, "A-w", package = pkg);
    clone_aur_repo(None, &p).ok_or(pkg)?;
}
let clones: Validated<(), &str> = packages.par_iter().map(clone).collect();

Everyone's allowed to disagree, but it seemed to me that he hadn't understood the original motivation and discussion.

Thank you for those examples, I can see the appeal. Personally I wouldn't choose to rely on macros here though. My gut feeling is that the function signature should be the source-of-truth-at-a-glance.

What about using use drop as void; and thing.map(void).map_err(From::from)? You also get more control on whether to use From or not use From with this.

2 Likes

For sure this is an existing possibility. If we consider that to be a "common capturable pattern", then introducing a shorthand like void could make sense (at least, that was my original motivation). Plenty such one-liner patterns exist as convenience methods in std. And if given an official method, it could be documented, tested, etc etc.

I don't think anyone disagrees overly with that last part. Don't think of the macros as a crutch, in this case they are very directly being used to explore the problem space of error handling. Error handling is a super important part of any language's story and error handling in Rust has definitely not achieved its final form.

  1. Are the error handling macros super useful and general and commonly used (e.g. dbg!)? -> Let's move them to std!
  2. Are the macros super useful, but expose some ugly corner cases that could be easily fixed by language level changes? Error handling is an important use case for sure. -> Let's consider changes to the language that could make this story better.
  3. What about outstanding changes to the language, like try?

The throws macro seems to do most of what you're asking for with a slightly different syntax.

By the way with anyhow I would write your example as

use anyhow::Result;

fn foo() -> Result<()> {
    let a = bar()?;
    let b = baz()?;
    work(a, b)?;
    Ok(())
}

which actually seems pretty close to using .void().

2 Likes

My dream is to do away with the Ok(()) here, which I'm arguing is smell. I actually like anyhow and use it in a number of crates. You're right that Rust as a whole hasn't reached its final answer w.r.t. Error handling, so I'm glad we can have these discussion and evolve the language proactively.

Error handling aside, is there any strong objection to a canonical fn void(self) -> Result<(), E> for the purposes of ignoring return values as described in the original posting (i.e. in lambdas or for Command::status)?

You will find many, many people who consider it valuable, and not at all a smell. See all the threads about try fn, or even just whether try blocks should success-wrap the body of the block.

?; Ok(()) vs .void() just doesn't seem that valuable, especially since it's so specific to ().

17 Likes

FWIW, I believe I did understand it completely, but, I didn't think this misunderstanding was worth derailing the topic.

But to be sure, I did reread the thread immediately after your response, and the one nuance I noticed was that your let clones example would have benefited from fehler supporting closures, which it doesn't seem to currently. I hadn't responded immediately to avoid being reactive, but then life caught up with me and I never got back to it (well, until now, I guess :smiley:).

Edit: Also, while I do prefer not going out of my way just to avoid Ok(()) and I'm unlikely to use fehler myself, I do recognize that there are others in the opposite camp, including the author of fehler. That's actually the principal reason it occurred to me to point his work out.

In a similar vein, you too could publish a crate that implements void() for all Result types using an extension method and see how broadly useful it is. Actual use may help motivate your proposal more convincingly.

1 Like

The word "smell" usually refers to code patterns that are indicative of a design error or programming risk. It doesn't typically refer to perfectly common, well understood, safe constructs that we just happen to not like the spelling of.

2 Likes

Pardon me if my initial response seemed flippant, that wasn't my intention. I probably also misunderstood your replies myself. Thanks for circling back to this.

re: fehler. I see now that I does a decent job at addressing my woes concerning Ok(()), etc., if I'm willing to accept macros and indirection w.r.t. the at-a-glance return type in the function signature.

I have actually done just that (minus a separate crate). The ResultVoid trait I showed in the original post (and the rest of the examples, for that matter) are lifted from a real-world project. But forcing an extra trait import on the user just to get access to void seems icky too, hence I'm here proposing its direct addition to std.

Thanks. I had mentioned in the original posting that a function-final Ok(()) in Rust is equivalent to a function-final pure () in Haskell, which is universally given a lint warning. In both cases, type-wise, such a line is strictly unnecessary, whatever other benefit it may have (hence my diagnosis; I claim that this isn't merely a matter of personal taste). If we're collectively engaged in the pursuit of detecting common patterns and abstracting them away, then this seems to me to be fertile ground for exactly that. fehler does seem to address this particular point well, now that I look closer, but it has its costs.