Restricted enum newtypes allow the type system to guarantee a value only uses some of the possible enum variants, while retaining an identical memory representation to the unrestricted type it’s derived from. The main use-cases include the obvious case such as std::io
functions returning only a subset of std::io::ErrorKind
, but also for utilizing uninitialized by restricting an Option<T>
-like type, and a third case I’ll get into later.
The definition and obvious case
struct ErrorKindGetMetadata(ErrorKind);
impl RestrictedEnum for ErrorKindGetMetadata {
type Unrestricted = ErrorKind;
fn restrict(x: &Self::Unrestricted) -> &Self {
match x {
NotFound | PermissionDenied => x,
_ => panic!(),
}
}
}
This syntax serves better as a description of how type restriction works, an actual implementation would probably be more terse (and wouldn’t allow arbitrary operations in restrict(..)
). It would be theoretically compiled into the code above, but in actuality would use compiler magic to disallow any call where the panic!()
case is not proved to be impossible, and it would be a no-op.
Safe, uninitialized memory
…works with a new definition for MaybeUninit
(perhaps by a different name)
// Notice that this is private
enum MaybeUninit<T> {
Uninit,
Init(T),
}
// Perhaps this colon syntax works
pub struct Uninit<T>(MaybeUninit<T>): Uninit;
pub struct Init<T>(MaybeUninit<T>): Init;
Newtypes named after enum variants (if allowed) should only be allowed if they restrict the enum to their eponymous variant.
let mut empty_buf: [Uninit<u8>; 1500] = [Uninit; 1500];
let mut (used, free): (&mut [Init<u8>], &mut [Uninit<u8>]) = recv(&mut empty_buf)
The compiler knows the only possibility for the values in empty_buf
are Uninit
, just as it knows that x: ()
must be ()
, and it doesn’t have to read any memory to get the value. The MaybeUninit
enum is private so that you can’t accept, return, store, etc. values of type MaybeUninit<T>
, because it’s unsafe to read a value of Uninit
, and even if you could there’s no tag distinguishing it from Init<T>
. (MaybeUnint
would therefore be both an “enum” and a union, and perhaps could be generalized instead of just using pub/private access controls).
The third case
…is having a struct with fields that may store erroneous values, but shouldn’t need cloning or unnecesary unwrapping when changing between a struct with all valid fields and the other cases.
struct AllGood {
x: Ok<u8, u16>,
y: Ok<u16, u8>,
z: Ok<u8, u8>,
w: u8,
}
#[derive(Restrict<AllGood>)]
struct SomeErrors {
x: Result<u8, u16>,
y: Result<u16, u8>,
z: Result<u8, u8>,
w: u8,
}
The Ok
types would implement Deref
or something like it to transparently act like their contents, while possibly taking more space. The relationship between the two structs is marked with the derive
thingy which would use the Restrict
trait from each of the implementing fields in AllGood
on the corresponding ones in SomeErrors
. There could be some heuristic that allows AllGood
to be entirely declared by the derive
. Any functions implemented on AllGood
could automatically return Option
when called on SomeErrors
. And the conversion of SomeErrors
to AllGood
can be optimized with a Result
-like type with a shared flag field.
This may have some relation to Types for Enum Variants: https://github.com/rust-lang/rfcs/pull/1450