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:
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.
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
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.
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.
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.
type MyErrorSubset = MyError is MyError::NotFound | MyError::PermissionDenied;
// or in an even brighter future...
type MyErrorSubset = MyError is _::NotFound | _::PermissionDenied;
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.
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].
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. ↩︎
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. ↩︎
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. ↩︎
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.
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]
And in the future perhaps for layout, though that sounds way more complicated. ↩︎
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). ↩︎