Skip to content

Latest commit

 

History

History
269 lines (211 loc) · 10.4 KB

File metadata and controls

269 lines (211 loc) · 10.4 KB

Goof

An early composable, and re-usable library for tiny structs that you would be writing on your own, but which you really shouldn't.

use goof::{Mismatch, assert_eq};

fn fallible_func(thing: &[u8]) -> Result<(), Mismatch<usize>> {
	assert_eq(&32, &thing.len())?;

	Ok(())
}

assert_eq!(fallible_func(&[]).unwrap_err(), assert_eq(&32, &0).unwrap_err())

So why use it? It's pre-alpha, so it's not particularly useful yet. But imagine a situation in which you don't really want to panic on failed assertions. These functions can then be a lightweight 1-1 replacement of the standard library `asserteq!` macro. It will not panic immediately, but instead create a structure called rustsrc{Mismatch}, which has all of the appropriate traits (except std::error::Error{.verbatim} because I want to add std{.verbatim} support, rather than subtract it) implemented.

These will participate in all manner of goodies, that don't necessarily depend on std{.verbatim}, but that can effectively make goof{.verbatim} a one-stop-shop for all your error handling needs. It will hopefully be as useful as eyre{.verbatim} and thiserror{.verbatim} while providing a slightly different approach to designing error APIs.

Goals

Drop-in thiserror replacement

We can do everything that thiserror can do, minus syn and quote dependencies. Why would you want to use this? Hopefully I can make it compile faster, so you have less guilt when adding it to your code-base.

But since thiserror has to remain stable, I can implement things that thiserror cannot, for example, I support ::core::error::Error, meaning you can use goof::Error in [no_std] contexts.

TODO: Another thing that you might find interesting is that you can use goof::Error to automatically create sensible doc-strings for your code. A lot of the time, I found that the doc-comments for Error enum variants just repeat the error messages.

Ready made error types

You get some ready-made error types, to cut out the need for custom repeated boilerplate. You have no idea HOW MANY TIMES, I had to create a structure that said, expect: 1, got: 2. I have non-panicking asserts which can be very useful if applied correctly. You have Mismatch<T>, Outside<T> and a few others to help.

TODO: Log-and-forget

Most errors aren't really something that you intend to process. A lot of the time, you know

Anyhow replacement

You have eyre and you have color_eyre. But you also sometimes want to have something that is in-between Box<dyn Error> and that. One crate to rule them all, and in the terminal print them. I don't plan to be fancy, I plan for you to be able to use what you want, similar to how tracing works, but simpler.

Copy-friendly

Did you ever want to make a structure Clone but found out that std::io::Error was not Clone, because some people decided that they wanted to be lazy, and somehow that code was still accepted into the standard library? The right thing to do, is to convert a std::io::Error into an information-preserving analogue, that is also Clone. I give you that.

Structs that could be copy, are.

Mishap, which is our equivalent of eyre::Report is Clone.

TODO: FFI-friendly

This is why I needed this. Picture a situation where you need to use no_std to reduce the size of a binary. But you can't without adding a bunch of boilerplate, because the representations on some of the structures on the cold-path are not solidified.

Worry no more. goof::Error is opt-out for repr(C). Why? Because the reason why Rust chose to make all structures ABI-unstable by default, is because it wanted to have some leeway for performance optimisations, at the cost of API stability. Errors happen infrequently, and correct and flexible error handling is much more important than performance on a cold path. So being able to pass a number of errors through the FFI boundary is actually useful.

TODO: Goof Fallible

A lot of the time, you are writing a function, but have to go back and forth between different code locations to co-design an error type. Often, you will just use anyhow, losing the ability to meaningfully handle the error, and the nice functional-style match semantics, instead getting opaque unsightly kind and downcast patterns all over your code.

I propose generating an ad-hoc Error enum on the spot. How does that work?

You annotate the function with #[goof::fallible]

Inside the function whenever you want to return

  • a unit variant, you call goof::bail!("Error text": VariantName).
  • a captured variable, goof::bail!("Error text with {variable:?}": VariantName{variable}), the type must be declared explicitly let variable: Type = value and implement std::fmt::Debug

Forwarded errors work a bit differently. Rust's ? operator knows the underlying type of the error being thrown, and if procedural macros worked a bit more like Lisp, we would too. Alas, we have to annotate the types. Fortunately, we can infer it, in a few cases.

	read.map_err(ConfigError::Read)?;

gives us just enough information to figure out that the return type can be ConfigError. This will break if ConfigError is generic, so you'd have to use the turbofish... sorry.

Another thing you can say is wrap_err. This signals to the macro that you are using Mishap, and so it will simply wrap that. This also disables the auto-generated #[from] annotation.

Let's say you want to wrap an existing error, preserve its type, but differentiate different conditions: for example, you are loading three files, and you want to wrap the std::io::Error, but differentiate which file failed to load; in that case you should use the following syntax:

#[goof::fallible]
fn init_program() -> Result<Config, InitError> {
    let config = read_file(CONFIG).map_err(InitError::ConfigNotFound)?;
    let cache = read_file(CACHE).map_err(InitError::CacheNotFound)?;
    config.init_cache(cache).map_err(InitError::CacheInit)?;
    config
}

If you feel that your compile times have tanked too much, you can expand the macro and emplace it.

TODO: Conversion graphs

Let's say you have a bunch of errors that have the same variant. You could unify them into a god-enum that processes every error case; it cleans up your "throwing" code, at the cost of handling errors being insufferably difficult. Your god-enum can only have traits which appeal to the lowest common denominator, so if 99% of your variants are Clone and Copy, but that one obscure variant is not, suddenly you made 99% of your program's use much slower.

The solution?

Smaller enum variants for specific error modes. But that can blow up into a huge zoo of enums and conversions, which most programmers mostly avoid, by having a Matrioshka of error variants.

The solution? goof::conversions!. This macro generates the appropriate TryFrom implementations with a well-defined error type, that takes the weight off your shoulders. Case in point:

goof::conversions!  {
	// Simple one variant to one variant rules.
	ConfigError => LargerError {
		::FileNotFound(e) => ::ConfigFileNotFound(e);
		// Convert a `ConfigError::ConfigVariableRedefined` into `LargerError` by
		// calling its associated method `LargerError::redefinition` with
		// the arguments passed in sequentially.
		//
		// We can infer that this creates the variant `LargerError::Redefinition`,
		// which we neeed to tally the variants.
		::ConfigVariableRedefined {
			varname: String,
			first_def: usize,
			second_def: usize
		} => ::redefinition(*fields);
	}

	InitError => LargerError {
		// Convert the three variants, into the same-named variants of `LargerError`
		//
		// The tuple type can specify the names of the arguments
		// (`StartDisplay`) or could specify the number of the
		// arguments, as in `AddPanicHook`. If the variant wraps
		// another struct we can use another pair of :: to destructure
		// and use the inner object `reason` in the
		// `LargerError::StartLogging` variant.
		::{StartDisplay(e), ::StartLogging{ reason }, AddPanicHook(#3)} => *variants
	}

	std::io::Error => ConfigError {
		// This will convert a `std::io::Error` into `AddrInUse` variant if and only if
		// the conditional is true.
		if matches!(self.kind(), ErrorKind::AddrInUse) then ::AddrInUse
		// Same, but with short-hand notation
		// This roughly translates to:
		// - For an enum whose name is `std::io::error::ErrorKind`
		// - if the kind is one of the following three primitive variants
		// - Embed the `std::io::Error` as a whole into `ConfigError::FileNotFound` variant
		if.kind(Self+Kind::{UnexpectedEOF, PermissionDenied, InvalidFilename}) => ::FileNotFound(self)
		// Same, but with short-hand notation
		// This roughly translates to:
		// - For an enum whose name is `std::io::error::ErrorKind`
		// - if the kind is one of the following three primitive variants
		// - Embed the `std::io::Error` as a whole into `ConfigError::FileNotFound` variant
		if.kind(Self+Kind::{UnexpectedEOF, PermissionDenied, InvalidFilename}) => ::FileNotFound(self)
	}
}

This should reduce the amount of repeated boilerplate.

TODO: No-nonsense

Warn the user about common anti-patterns in Error design.

  • Called your enum Error, get a warning saying that this is bad: why? Because I am fed up with having to rely on the LSP to tell me next to nothing about what kind of error I'm dealing with. Inlay hints help a lot, and you just made their job way harder.
  • God enum Error with a bunch of common prefixes. You probably want a bunch of smaller enums with conversion methods.

Progress

The library is in its early stages. I'm planning on approaching this from the minimalist perspective, of making a bunch of 0.*{.verbatim} versions and when the library is complete, releasing the 1.0{.verbatim} version. While this is in no way a pre-release candidate and as is, it should be ready for production use, I would recommend not spending too much time worrying about the changes in the newer versions. Update as you see fit, if you do, I will be providing detailed notes on how to make the jump.

Changelog

  • 0.1.0
    • Initial, extremely basic implementation of Mismatch{.verbatim}, Outside{.verbatim} and Unknown{.verbatim} structures.
    • Initial implementations of assert_eq{.verbatim}, assert_in{.verbatim}, assert_known_enum{.verbatim}, and assert_known{.verbatim}.
  • 0.2.0
    • Swapped around arguments in assert_eq{.verbatim} for more consistency.