F-pattern_types : Subsets enums using as

In error handling it is common to see large error enums where a function of type Result<T, E> will exist where only a subset of the error enum E enum can be reached by the function. Currently, the only solution to make the variants specific to that function is to declare a new error enum which may have duplicates of the variants in the original enum. There are two ways to make errors specific to the functions which may return them:

  1. You take an enum with lots of errors, and then have some way of declaring "only these specific variants are possible here". That's pattern types.
  2. You have distinct error types, and then combine them to declare which ones can be returned. That's union types. (Seen in TS, and the throws declaration in java)

Both approaches basically let you write a list of specific error types that are allowed, without having to declare hundreds of slightly different enums

Credit: u/Deadmist

Right now I cannot see a clean way of doing this in Rust. However one approach which would be nice is:

#[derive(Debug, Error)]
pub enum MyError {
    #[error("not found")]
    NotFound,

    #[error("invalid state")]
    InvalidState,

    #[error("permission denied")]
    PermissionDenied,
}


pub enum MyErrorSubset {
    MyError::NotFound,
    MyError::PermissionDenied,
}

Justification for why error specification is better than glob error enums: thinking of function signatures as contracts falls apart when a return type is unreachable and is a form of deadcode. If you cannot elicit an variant of an enum from a function, then it is bad practice to have that variant in the enum.

The search term is "pattern types". You can find some previous discussions on this topic.

Outside of pattern types, you might consider using error_set.

I see this:

Which after digging on the closed and unmerged MVP PR brings up this:

Which was left @

In June 2023.

There's an older thread from 2018 which mentioned this and ended as:

However I cannot find the newer thread, the one that mentioned it was about floating point rounding. Is it this commit: Rollup merge of #123648 - oli-obk:pattern_types_syntax, r=compiler-er… · rust-lang/rust@727c31a · GitHub

Does that mean it is in the current version of Rust? How do we use it?

Ahhh... It seems it is internal only and worked only for ranges: Implement minimal, internal-only pattern types in the type system by oli-obk · Pull Request #120131 · rust-lang/rust · GitHub

So basically, this is all still in flux and not currently being worked on? Not saying that is negative, I am just trying to understand where things are at the moment.

1 Like

I have not seen this, thank you!

As a huge fan of pattern types, I hope I'll be lucky enough to see them as a stable language feature in my lifetime.

Here's the tracking issue: Tracking Issue for pattern types · Issue #123646 · rust-lang/rust · GitHub

A lot of good work has gone into them, but my understanding is we're a very long way from them becoming a thing outside of the compiler (if ever).

3 Likes

Based on the syntax supported by pattern types (when they emerge) would my example be re-worked to be like:

#[derive(Debug, Error)]
pub enum MyError {
    #[error("not found")]
    NotFound,

    #[error("invalid state")]
    InvalidState,

    #[error("permission denied")]
    PermissionDenied,
}


pub enum MyErrorSubset {
    NotFound is MyError::NotFound,
    PermissionDenied is MyError::PermissionDenied,
}

This seems like making the parent a repr type and using as.

I see, well I was not aware of pattern types two hours ago and then when presented with them I though "Oh this is great how do I enable unstable_pattern_types?" So this has been a rollercoaster learning we are far from them! Haha, thanks for the hard work rust-lang contributors, if you are here someday in the future.

To clarify, they can be used unstably, but they are very minimal. You'd need to use a macro to create them (or transmute). I tried previously, but they were just too unstable for me to really make use of.

I am saying we are a long way from them being stable, since currently, there are no plans for them to ever be stable that I am aware of.

I think the features you'd need are:

#![feature(pattern_types, rustc_attrs)]
#![feature(core_pattern_type)]
#![feature(core_pattern_types)]
#![allow(incomplete_features)]

...but it has been a while, so I might be mistaken.

type MyErrorSubset = MyError is MyError::NotFound | MyError::PermissionDenied;
// or in an even brighter future...
type MyErrorSubset = MyError is _::NotFound | _::PermissionDenied;

I would guess something like

type MyErrorSubset = MyError is (MyError::NotFound | MyError::PermissionDenied);

The hard question is whether Result<T, MyErrorSubset> can actually be any smaller if it's defined that way.

It's possible it'll have to be

struct MyErrorSubset(MyError is (MyError::NotFound | MyError::PermissionDenied));

in order for Result<T, MyErrorSubset> to actually take layout advantage.

(This kind of thing is one of the reasons it'll take a while to be stable.)

1 Like

You put this into the "Unsafe Code Guidelines" category. What does this have to do with unsafe code?

Ahh that was a mistake, mis-input.

I believe types as enum variants would solve this use case with "just" some syntactic sugar.

@rustbot label +F-pattern-types

In my opinion, the main benefit to pattern types is in exhaustiveness checking in e.g. match constructs and other static guarantees, though it'll be a nice extra if e.g. Result<NonZeroU32, MyError is MyError::NotFound> gets optimized to the same size as a u32. An initial implementation which handles the language semantics, but leaving open optimization opportunities for later, would be good enough for me to be happy.

3 Likes

FWIW, the example as currently spelled in the OP

seems like it's a combination of "enum variants are types" and "types as enum variants." The "pattern restricted subtype" spelling

feels to me like it would be the most commonly used option in a world with all options available, as it asks for the simple type restriction of MyError to the possible variants without any layout changes (thus true no-op conversion to MyError) and inheriting all (immutable) trait functionality automatically.

With pattern restricted subtypes, the "enum variant as type" MyError::NotFound would likely be spelled MyError is MyError::NotFound. But "types as enum variants" still gives the tuple struct equivalent for enum types where the variants are indexed instead of named, and while this is technically just sugar[1], I believe it's sugar that would make "newtype variants" nicer to work with[2].


In any case, pattern restricted types are stalled because they're difficult. We want to inherit all functionality from MyError to MyErrorSubset, but while &MyErrorSubset ⊆ &MyError, &mut MyError ⊆ &mut MyErrorSubSet, and also &MyError ⊆ &mut MyError and &MyErrorSubset ⊆ &mut MyErrorSubset, so it's a complicated mess, and internal mutability makes things even more complicated… knowing what associated functions and trait impls can be provided on the subset type is a very thorny problem, and it gets even more thorny once you start considering forwards compatibility with API evolution that we deem semver compatible.

But even given all of that, pattern restricted types are super useful, and I hope I get to see Rust gain a version of them within my career, even if it's massively restricted[3].


  1. Whether it's just sugar or not depends on whether the type has sum type semantics (multiple variants using the same type are distinct) or union type semantics (multiple variants using the same type are unified). In the face of generics, I believe that sum type semantics are the consistent choice. ↩︎

  2. In short, enum EnumType { use VariantType } would be sugar for enum EnumType { 0(VariantType) }, which we'd likely write today as enum EnumType { Variant(VariantEnum) }. Pattern matching would use the index or, in the most ideal case, allow using type ascription syntax when that is unambiguous. ↩︎

  3. A version which only allows pattern restricted types for the field of newtype structs and requires unsafe for accesses to the inner field (thus manual and unsafe forwarding of any traits) simply as a way to spell custom niches in user code would be wonderful, even without further niceties that it could theoretically enable. ↩︎

1 Like

My thought exactly and the reason I brought this up.

Prior discussion: Memory-layout-compatible enum/struct subsets

I would personally be happy with an MVP, something that just works for scalars and newtypes around scalars for example. Maybe payload-less enums too. And niche optimisation could be improved over time to handle more cases.

1 Like

Correct me if I'm wrong, but isn't Subtyping basically the same problem, just on lifetimes? Without further niche and layout optimizations (that change the memory layout) I do not see much of a difference conceptually (though in the implementation there probably is a very large difference). It could probably be done by using the parent type but adding a subtype_id to it that is used for pattern match exhaustiveness checks. [1]

That's mostly a question of whether you want to allow getting a &mut MyError from a MyErrorSubset or &mut MyErrorSubset, right? I suspect there is no good answer to that question, as both cases are valid and useful. I don't even know how many of the people (myself included) asking for pattern types want/need which one. [2]


  1. And in the future perhaps for layout, though that sounds way more complicated. ↩︎

  2. Perhaps needing both options and a way to choose whether you want &mut Parent or a smaller memory footprint (e.g. for u64 is 100_000..100_050, which could be a u8 and/or have many niches). ↩︎