Stream: general

Topic: sleek derive(Error)


Jake Goulding (Jan 27 2019 at 03:42, on Zulip):
#[derive(Debug, MyError)]
enum Error {
    #[my_error::display("Could not open config file at {}: {}", self.filename.display(), self.source)]
    OpenConfig { filename: PathBuf, source: io::Error },
    SaveConfig { source: io::Error },
    MissingUser,
}

type Result<T, E = Error> = std::result::Result<T, E>;

fn do_it(fail: bool) -> Result<()> {
    let filename = "/tmp/config";

    let _f = fs::read(filename).context(OpenConfig { filename })?;
    fs::write(filename, "config").context(SaveConfig)?;

    if fail {
        return Err(Error::MissingUser);
    }

    Ok(())
}

Check out my sweet new Error-type-boilerplate-reduction-derive-macro thingie

Jake Goulding (Jan 27 2019 at 03:43, on Zulip):

And now to bed.

nagisa (Jan 27 2019 at 09:26, on Zulip):

The failure crate has a very similar thing:

// taken from https://boats.gitlab.io/failure/
#[derive(Debug, Fail)]
enum ToolchainError {
    #[fail(display = "invalid toolchain name: {}", name)]
    InvalidToolchainName {
        name: String,
    },
    #[fail(display = "unknown toolchain version: {}", version)]
    UnknownToolchainVersion {
        version: String,
    }
}

is there a major difference here?

Jake Goulding (Jan 27 2019 at 14:49, on Zulip):

Yes: I didn't invent a brand new trait. Failure doesn't implement From AFAICT.

Jake Goulding (Jan 27 2019 at 14:49, on Zulip):

There's a reason I also included the usage sites

Jake Goulding (Jan 27 2019 at 14:50, on Zulip):

Failure's Display also forces the causal error to be printed in a certain way

Jake Goulding (Jan 27 2019 at 14:52, on Zulip):

Every time I've attempted to use failure (not many, TBH), I've felt that it wasn't designed by folk that actually used it.

Obviously that can't be true, but I've never once understood why people are so very in love with it.

Jake Goulding (Jan 27 2019 at 14:54, on Zulip):

One thing that failure states (obliquely) that I agree with is that directly supporting From<AnotherError> isn't a great pattern. Just because an IO error happened doesn't express anything useful. There's a context to that IO operation in my usage (in the example: reading or writing a file)

Jake Goulding (Jan 27 2019 at 14:55, on Zulip):

Which is why I like fs::read(filename).context(OpenConfig { filename })?

Jake Goulding (Jan 27 2019 at 14:56, on Zulip):

extending the current concept of "context" to mean both "some additional data beyond just the error type" and "the context of my code in which the underlying error happened"

nagisa (Jan 27 2019 at 15:00, on Zulip):

That’s what I do with "error chaining"

nagisa (Jan 27 2019 at 15:00, on Zulip):

(adding context)

nagisa (Jan 27 2019 at 15:00, on Zulip):

I even began using this concept in C++ and errors there, implementing casts and constructors between different kinds of errors to collect the context.

Jake Goulding (Jan 27 2019 at 16:27, on Zulip):

Do you use failure? If so, can you provide an equivalent construct? I could not figure out how to do it.

Pietro Albini (Jan 27 2019 at 16:30, on Zulip):

do you mean

fs::read(filename).with_context(|_| OpenConfig { filename })?

?

nagisa (Jan 27 2019 at 16:31, on Zulip):

I may have used failure a while ago, not sure in which project though. IIRC what I would usually do is fallible().context(format!("my nice error message and some formatted useful info") IIRC.

nagisa (Jan 27 2019 at 16:32, on Zulip):

ah it was internal work project IIRC.

nagisa (Jan 27 2019 at 16:32, on Zulip):

can’t check the code there now

Pietro Albini (Jan 27 2019 at 16:32, on Zulip):

with failure (on crater) I just use with_context everywhere

Jake Goulding (Jan 27 2019 at 16:32, on Zulip):

So that'd be using stringly-typed errors, yes? Maybe OK for an app, not the best for a library?

Pietro Albini (Jan 27 2019 at 16:32, on Zulip):

to avoid slowing down things where stuff is ok

Pietro Albini (Jan 27 2019 at 16:33, on Zulip):

you can of course use typed errors as contexts

Pietro Albini (Jan 27 2019 at 16:33, on Zulip):

on crater I don't need them to be typed though

nagisa (Jan 27 2019 at 16:33, on Zulip):

yeah, in my case I was making an executable as well (rather than a library)

nagisa (Jan 27 2019 at 16:34, on Zulip):

for libraries I tend to avoid any error handling crates entirely

Jake Goulding (Jan 27 2019 at 16:34, on Zulip):

Why?

nagisa (Jan 27 2019 at 16:34, on Zulip):

No idea... my libraries are all on the small side and not pulling in such a crate feels more lightweight

nagisa (Jan 27 2019 at 16:35, on Zulip):

I guess.

Jake Goulding (Jan 27 2019 at 16:35, on Zulip):

@Pietro Albini how would you create those typed contexts? More applications of deriving Fail?

Pietro Albini (Jan 27 2019 at 16:35, on Zulip):

yep, mostly the same as your derive

Pietro Albini (Jan 27 2019 at 16:35, on Zulip):

one sec

Pietro Albini (Jan 27 2019 at 16:36, on Zulip):

https://github.com/rust-lang-nursery/crater/blob/master/src/utils/hex.rs for example

Jake Goulding (Jan 27 2019 at 16:38, on Zulip):

... you wrote your own hex decoding?

Pietro Albini (Jan 27 2019 at 16:39, on Zulip):

didn't want to pull a crate just for 30 lines of code :(

Pietro Albini (Jan 27 2019 at 16:39, on Zulip):

crater already has something like 300 deps

Pietro Albini (Jan 27 2019 at 16:39, on Zulip):

(counting deps of the deps)

Jake Goulding (Jan 27 2019 at 16:39, on Zulip):

I shan't ever understand you folk.

Jake Goulding (Jan 27 2019 at 16:39, on Zulip):

One of those is probably the hex crate... If so, using the dependency would make compiling faster

Jake Goulding (Jan 27 2019 at 16:40, on Zulip):

Looks like 60 lines because you wrote tests, because it wasn't tested by other people

Jake Goulding (Jan 27 2019 at 16:41, on Zulip):

Anyway, I'm missing how this example corresponds to my code above.

Jake Goulding (Jan 27 2019 at 16:42, on Zulip):

Let me start over, please.

Jake Goulding (Jan 27 2019 at 16:42, on Zulip):
let _f = fs::read(filename).context(OpenConfig { filename })?;
fs::write(filename, "config").context(SaveConfig)?;

Can you show the equivalent using failure? i.e. "this io::Error means something went bad for opening a file, this io::Error means something when bad saving a file"

Jake Goulding (Jan 27 2019 at 16:43, on Zulip):

Which would get bucketed into

enum Error {
    OpenConfig, // whatever you need, presumably an io::Error is inside here
    SaveConfig, // whatever you need, presumably an io::Error is inside here
}
Pietro Albini (Jan 27 2019 at 16:43, on Zulip):
let _f = fs::read(filename).with_context(|_| ConfigError::OpenConfig { filename })?;
fs::write(filename, "config").context(ConfigError::SaveConfig)?;
Pietro Albini (Jan 27 2019 at 16:44, on Zulip):

and the context definition is the same one you would use for an error

Pietro Albini (Jan 27 2019 at 16:44, on Zulip):

for example that hex error enum

Jake Goulding (Jan 27 2019 at 16:45, on Zulip):

what does the error type (an enum?) that contains either of OpenConfig / SaveConfig look like?

Pietro Albini (Jan 27 2019 at 16:46, on Zulip):
#[derive(Debug, Fail)]
enum ConfigError {
    #[fail(display = "failed to open configuration file {}", filename)]
    OpenConfig { filename: String },
    #[fail(display = "failed to save the configuration file")]
    SaveConfig,
}
Jake Goulding (Jan 27 2019 at 16:50, on Zulip):

Now I'm missing something... how does the struct OpenConfig (which is also derive(Fail), right?) get converted to ConfigError::OpenConfig?

Jake Goulding (Jan 27 2019 at 16:51, on Zulip):

and where does the underlying cause (the io::Error) get stashed?

Pietro Albini (Jan 27 2019 at 16:52, on Zulip):

Now I'm missing something... how does the struct OpenConfig (which is also derive(Fail), right?) get converted to ConfigError::OpenConfig?

uh... it's actually ConfigError::OpenConfig, woops

Jake Goulding (Jan 27 2019 at 16:53, on Zulip):

So is is actually using with_context then?

Jake Goulding (Jan 27 2019 at 16:53, on Zulip):

cause a Context<io:Error, ConfigError> presumably cannot be converted to a ConfigError...

Pietro Albini (Jan 27 2019 at 16:54, on Zulip):

yep, but it can be converted to a Box<Fail> or Error

Pietro Albini (Jan 27 2019 at 16:54, on Zulip):

you can implement contexts manually if you want to avoid the boxing

Pietro Albini (Jan 27 2019 at 16:55, on Zulip):

never did that, but I think you can do something like

#[derive(Debug, Fail)]
enum ConfigError {
    #[fail(display = "failed to open configuration file {}", filename)]
    OpenConfig { filename: String, #[cause] cause: io::Error },
    #[fail(display = "failed to save the configuration file")]
    SaveConfig { #[cause] cause: io::Error }
}
Pietro Albini (Jan 27 2019 at 16:56, on Zulip):

you won't be able to use the .with_context method though

Jake Goulding (Jan 27 2019 at 16:58, on Zulip):

which also means you can't use ?, right?

Pietro Albini (Jan 27 2019 at 16:58, on Zulip):

no

Jake Goulding (Jan 27 2019 at 16:59, on Zulip):

What's the equivalent of fs::read(filename).context(OpenConfig { filename })? for this most recent enum?

Pietro Albini (Jan 27 2019 at 17:00, on Zulip):
fs::read(filename).map_err(|io| ConfigError { filename, cause: io })?;
Pietro Albini (Jan 27 2019 at 17:00, on Zulip):

I think

Pietro Albini (Jan 27 2019 at 17:00, on Zulip):

haven't used an enum like that though

Jake Goulding (Jan 27 2019 at 17:02, on Zulip):

so failure isn't providing us anything for that line, as that's just a struct literal and Result::map_err

Jake Goulding (Jan 27 2019 at 17:03, on Zulip):

Also, you totally got to write it as map_err(|cause| ConfigError { filename, cause })?; :wink:

Pietro Albini (Jan 27 2019 at 17:03, on Zulip):

yeah, if you want .context you have to use the provided Context struct

Jake Goulding (Jan 27 2019 at 17:04, on Zulip):

compare:

// failure
foo().map_err(|cause| ConfigError { filename, cause })?;
// to-be-named
foo().context(OpenConfig { filename })?;
Pietro Albini (Jan 27 2019 at 17:04, on Zulip):

I'm technically doing other things right now :stuck_out_tongue:

Pietro Albini (Jan 27 2019 at 17:04, on Zulip):

I'd prefer if context was a closure though

Pietro Albini (Jan 27 2019 at 17:04, on Zulip):

or at least support closures there

Pietro Albini (Jan 27 2019 at 17:05, on Zulip):

and how do you store the underlying cause?

Jake Goulding (Jan 27 2019 at 17:05, on Zulip):

I mean, that's a straight-forward addition, and orthogonal. Failure supports both (context / with_context)

Jake Goulding (Jan 27 2019 at 17:05, on Zulip):

(and I added it to my TODO, as well)

Jake Goulding (Jan 27 2019 at 17:06, on Zulip):
#[derive(Debug, MyError)]
enum Error {
    OpenConfig { filename: PathBuf, source: io::Error },
    SaveConfig { source: io::Error },
Jake Goulding (Jan 27 2019 at 17:06, on Zulip):

The cause is right there ^

Jake Goulding (Jan 27 2019 at 17:06, on Zulip):

using that fresh new name of "source"

Pietro Albini (Jan 27 2019 at 17:07, on Zulip):

so you have a struct OpenConfig that you then convert to Error::OpenConfig?

Jake Goulding (Jan 27 2019 at 17:07, on Zulip):

you are correct

Pietro Albini (Jan 27 2019 at 17:07, on Zulip):

hmm

Pietro Albini (Jan 27 2019 at 17:07, on Zulip):

could be nice

Jake Goulding (Jan 27 2019 at 17:07, on Zulip):

(it's been pointed out that a derive creating new types is not an obvious use, so I'll probably switch to a plain proc-macro)

Jake Goulding (Jan 27 2019 at 17:08, on Zulip):

And of course, this all needs docs with prose

Jake Goulding (Jan 27 2019 at 17:08, on Zulip):

But pasting it here has helped me figure out the "value proposition" as well as what needs to be explicitly pointed out

Jake Goulding (Jan 27 2019 at 17:09, on Zulip):

And prevents me from releasing purely duplicate crates ;-)

Jake Goulding (Jan 27 2019 at 17:09, on Zulip):

Before I forget, thank you both @nagisa and @Pietro Albini !

Pietro Albini (Jan 27 2019 at 17:10, on Zulip):

is this going to be the third error handling crate on the nursery?

Jake Goulding (Jan 27 2019 at 17:10, on Zulip):

I also need to add some backtrace. Debating over

enum Error {
   #[my_error::backtrace]
   Alpha
   // vs
   Alpha { backtrace: my_error::Backtrace }
}
Jake Goulding (Jan 27 2019 at 17:11, on Zulip):

I don't know why it'd be on the nursery; most of the crates I've made have faded into obscurity. I don't have the position of authority to claim that my crate will replace the standard library and everyone should jump ship to it. :wink:

Pietro Albini (Jan 27 2019 at 17:12, on Zulip):

what about

enum Error {
    Alpha { ctx: Ctx<io::Error> }
}
Pietro Albini (Jan 27 2019 at 17:12, on Zulip):

and then include both the context and the backtrace on the ctx?

Pietro Albini (Jan 27 2019 at 17:12, on Zulip):

(name bikesheddable a lot)

Pietro Albini (Jan 27 2019 at 17:12, on Zulip):

having to add the backtrace to every error is going to be a pain

Jake Goulding (Jan 27 2019 at 17:15, on Zulip):

having to add the backtrace to every error

This is something I need to get a better sense of. For example, people may not want a backtrace generated for reasons. Also, you might want a backtrace generated even if there isn't an underlying cause (actually, this seems like the more likely case)

Jake Goulding (Jan 27 2019 at 17:15, on Zulip):

since ideally the underlying error has its own backtrace

Jake Goulding (Jan 27 2019 at 17:16, on Zulip):

which includes the current libraries code location by construction

Jake Goulding (Jan 27 2019 at 17:17, on Zulip):

So bundling them together seems like a non-starter

Last update: Nov 22 2019 at 00:05UTC