5 ms·
I'll hype my own library, SNAFU [1]. It simplifies constructing your own "leaf" errors and streamlines the ability of collecting multiple types of errors while
by shepmaster 5y ago
I'll hype my own library, SNAFU [1].
It simplifies constructing your own "leaf" errors and streamlines the ability of collecting multiple types of errors while attaching more context to them (e.g. filenames, stack traces, user ids, etc.). It allows you to smoothly switch from "stringly-typed" errors to strongly-typed errors. You can create opaque errors to avoid leaking internal implementation details into your public API.
Applied to the code in the post:
use snafu::prelude::*;
use std::{fs::File, io::prelude::*};
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Unable to open {filename}"))]
Opening {
source: std::io::Error,
filename: String,
},
#[snafu(display("Unable to read {filename}"))]
Reading {
source: std::io::Error,
filename: String,
},
#[snafu(display("Unable to parse {buffer} as a number"))]
Parsing {
source: std::num::ParseIntError,
buffer: String,
},
}
fn read_number_from_file(filename: &str) -> Result<u64, Error> {
let mut file = File::open(filename).context(OpeningSnafu { filename })?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)
.context(ReadingSnafu { filename })?;
let buffer = buffer.trim();
let parsed: u64 = buffer.parse().context(ParsingSnafu { buffer })?;
Ok(parsed)
}
The key parts are the `derive(Snafu)` on the definition of the error enum and the usages of `.context` and `XxxSnafu` at the error sites.
Importantly, this example demonstrates a key feature of SNAFU, here shown as "not all `io::Error`s are the same". Opening the file and reading the file are two separate error conditions and should not be lumped together as one.
[1]: https://docs.rs/snafu/0.7.0-beta.1/snafu/index.html https://docs.rs/snafu/0.7.0-beta.1/snafu/index.html
- dmix 5y agoKinda feels like putting types in the comments like JSDoc or Dialyzer. I don't use Rust enough to comment otherwise.
- db48x 5y agoIt’s a little similar, except that these are attributes rather than comments. They’re part of the syntax of the language, and new macros can be written to add new attributes. Attributes are used for several different purposes within the language, so using them is familiar and common. Rust also has two kinds of comments, one that shows up in the generated documentation and one that doesn’t.
- Measter 5y agoAs mentioned, these are attributes not comments. More specifically, the the `#[derive]` attribute on the enum lists which procedural macros to execute (Debug and Snafu here). These procedural macros are handed the token stream for that enum, and can use that to generate new code. The Snafu macro here is also using attributes on each variant of the enum for information about how to implement certain things. The documentation for Snafu has a page[1] describing what code is generated. There's nothing there that couldn't be done by hand, it's just tedious. [1] https://docs.rs/snafu/0.6.10/snafu/guide/the_macro/index.html https://docs.rs/snafu/0.6.10/snafu/guide/the_macro/index.htm...
- dmix 5y ago> More specifically, the the `#[derive]` attribute on the enum lists which procedural macros to execute (Debug and Snafu here). Yep I've used Rust before... It's still like putting comments above your functions.
- kalcode 5y ago> It's still like putting comments above your functions. If you're used to a certain language then sure that makes sense. Your comment seems like you are boxing yourself in, limiting yourself to just what makes sense in Javascript, very opposite of a programmer who looks to improve their craft. Take a moment and think about that. Because it looks like Javascript comment...you only see it like that to a point of making this statement. There is a lot of ways different languages uses tokens/symbols to indicate something. Not everyone agrees what those symbols are used for. Some languages use it to define macros like C++ or preprocessor directives like C#. Some as comments JS/Python/. Some as like Java nothing. Because the languages you use are use to it, and the language you use doesn't use something similar, it looks "wrong". That is in itself a narrow view. I think you should question that kind of thinking, I believe it will be helpful.
- bobbylarrybobby 5y agoSeems cool, but is a whole crate worth it for the `.context` function when you could just use e.g., `.map_err(|source| Error::Opening { source, filename })`? Seems like all `.context` provides is not not needing to name the originating error? (And obviously the `#[snafu(display(...))]` macros could just be moved into a `impl Debug for Error`.)
- shepmaster 5y ago> but is a whole crate worth it Yes. I'm not sure exactly what other response you'd expect from the author/maintainer of a library when they've already made a post encouraging other people to use it. ¯\_(ツ)_/¯ > when you could just use e.g., `.map_err(|source| Error::Opening { source, filename })` That's not equivalent, as `filename` is a `&str` but becomes a `String` when stored in the error. SNAFU automatically calls `Into::into` for you, so the closest would be: .map_err(|source| Error::Opening { source, filename: filename.into() }) > Seems like all `.context` provides There's also the possibility of automatic construction of values (backtraces, location information, the current time, things captured from globals / thread locals, etc.) Beyond `.context`, SNAFU also implements the `Error` trait (and associated methods like `Error::source`). There's also convenience methods and macros to create leaf errors, those that originate in your code. > obviously the `#[snafu(display(...))]` macros could just be moved into a `impl Debug for Error` I'll assume you mean `Display`, not `Debug`. That also not quite true, as SNAFU offers a shorthand syntax that isn't yet in stable Rust: "Unable to read {filename}" would need to be one of "Unable to read {}", filename "Unable to read {filename}", filename = filename > like all [...] provides [...] obviously [..] could just be moved All code could have been written by your own hand or otherwise inlined. Your response feels (needlessly) highly dismissive of another person's work.
- bobbylarrybobby 5y agoThanks for your reply. I guess my question should really have been a statement: “based on this example, I don’t think it’s worth it”. But the info you provided does make it seem worthwhile. Cheers
- OJFord 5y agoObviously I realise which you'll say is best, but any comment on snafu vs thiserror/anyhow? That's what I've used so far pretty much just because it seemed the popularly recommended way to solve the problem, but I wouldn't say it's been massively smooth. Also, it seems unfortunate you won't get autocompletion (the first time anyway) for (the 'Snafu') part of XxxSnafu.
- shepmaster 5y ago> snafu vs thiserror/anyhow I'd like to provide a fair comparison [1] in the documentation, but I don't know thiserror / anyhow well enough to feel like I'd give them the credit they are due. That said, to my knowledge, thiserror doesn't allow you to take an `io::Error` and sort it into two different enum variants (like the `Reading` and `Opening` variants in my grandparent example). To me, those are vastly different error states that just both happen to have the same error type. You can extend the metaphor with any larger error type from a crate (e.g. `reqwest::Error`). Anyhow requires using a trait object (and potentially downcasting) and I prefer avoiding those when possible. > seemed the popularly recommended way Absolutely. The author of those crates is a giant in the Rust community [2] (they are also the author of serde, syn, and quote, for example!). If those crates suit your situations, then by all means — use them. I'd much rather the Rust community have better error types and messages by whatever means available. Even using `String` via `Box<dyn Error>` is better than nothing. > you won't get autocompletion You should, at least if you use rust-analyzer. I use it via emacs and have these settings enabled, but I think they were going to be the default: (lsp-rust-analyzer-cargo-load-out-dirs-from-check t) (lsp-rust-analyzer-proc-macro-enable t) [1]: https://docs.rs/snafu/0.7.0-beta.1/snafu/guide/comparison/index.html https://docs.rs/snafu/0.7.0-beta.1/snafu/guide/comparison/in... [2]: https://crates.io/users/dtolnay https://crates.io/users/dtolnay
- rileyphone 5y agoThanks for the emacs tip! I've been dealing with that for a minute and here is the solution in hn comments, what serendipity.
- nagisa 5y ago