This is the latest( third ) version of the RFC
The following is the second version of the proposal.
Summary
Add macros and traits in std library, for minimal support of structural enum.
The new constructs are:
-
Four traits
FromVariant
,IntoEnum
,ExchangeFrom
,ExchangeInto
. -
A new proc-macro derive
Exchange
applicable forenum
s, generating type convertionimpl
s defined in these four traits. -
A declarative macro
Enum!
accessing to predefinedExchange
able enum.
Motivation
Programmers are used to divide a task into its compositional sub-tasks which are more detailing and easier to understand and implement. It makes programs organized as a hierarchy of functions. Some of the functions are dealing with the input directly, then sending the intermediate results to the functions focusing on a higher level. Then the latter ones accept the intermediate outputs as their inputs, and so forth.
This “output as input” data flow leads to one question: what is the type of a function’s output, if it accepts different kinds of intermediate results?
Two obvious answers to this question:
- Using
enum
.
The actual input types are reserved and reflected by the output type.
- Using
trait object
.
The actual input types are erased, and the output type is a public interface.
These two methods have significant differences:
-
Changes in upstream functions may propagate to downstream if using
enum
, while it won’t fortrait
object. -
It is easy to do down-casting if using
enum
, while not the case fortrait
object. -
All the possible actual input types must support the functions in the
trait
object, potentially leading to a fat interface.
Both methods are reasonable and have use cases which are most suitable for.
Lacking of structural enum type in Rust, it causes some unfavorable results:
- Suffering from boilerplate code for enum convertion, either handwritten or macro generated. But such convertions are non-trival to implement, in its general form. Further more, rustaceans are developing such non-trival equivalents in specific domains.
A notable example is “error-chain”.
It had reinvented the wheel for some kind of structural enum in the form of
ErrorKind
.
- Tempting programmers to use
trait
object instead, in the case for which usingenum
is most suitable for.
The shortcommings are
-
unneccesary boxing and
'static
lifetime bounds -
unneccesary trait methods, aka fat interface
Programmers will get a mandatory presumption that all types flowing from lower-level functions to high-level ones must have common methods predefined in a trait, which is not always true. It effectively leads to difficulties in defining the trait methods. Once the trait has been defined, programmers will eventualy find some useless methods in the trait. On the other hand, lacking some methods in the trait will ask for down-casting, but
-
non-straigthforward down-casting
Supporting some kind of structural enum type, even in its minimum form, will bring signficant benifits:
-
directly expressing the concept which is easily and well understood, rather than learning various equivalents in domain specific libraries resolving the same problem time and time again. And the library authors could focus on domain specific stuffs, not to reinvent the wheel for infrastructures.
-
aware of the abusing of trait object, and mind changing to get better designs.
-
its ability to reflecting function shapes helps programmers do local lookups rather than recursively tracing into inner functions causing a global search.
A concrete use case is error-handling. Applying structrual enums in error-handling reslults putting all error types in function signatures, aka
throws
.
However, introducing a fully-supported structural enum type tends be big changes in the language leading to lots of work. And it is not confident to decide what a structrual enum type ought to be the most ideal for Rust, without full experience. This proposal focuses on a minimum support for structural enum type, without actually introducing a new type concept. Instead, a set of traits and macros are proposed.
Guide-level explanation
A user-defined enum tagged #[derive(Exchange)]
is considered as an
exchangeable enum.
#[derive(Exchange)]
enum Info {
Code(i32),
Text(&'static str),
}
An exchangeable enum can be constructed from one of its variant:
let info: Info = 42.into_enum();
let info = Info::from_variant(42);
An exchangeable enum can be exchanged from/into another exchangeable one, as long as one has all the variant types appear in the other one’s definition.
#[derive(Exchange)]
enum Data {
Code(i32),
Text(&'static str),
Flag(bool),
}
let info: Info = 42.into_enum();
let data: Data = info.exchange_into();
let info = Info::from_variant(42);
let data = Data::exchange_from(info);
Note that all variants must be in the form of “newtype”.
#[derive(Exchange)]
enum Info {
Text(String), // ok, it is newtype
Code(i32,i32), // compile error
}
should cause an error:
1926 | Code(i32,i32),
| ^^^^^^^^^^^^^ all variants of an exchangeable enum must be newtype
Predefined exchangeable enums
The Enum!( T0, T1, .. )
macro defines an predefined exchangeable enum composed
of variant types T0, T1, … etc.
let info = <Enum!(i32,&'static str)>::from_variant(42);
is essentially equvalent to the following code:
let info = __Enum2::from_variant(42);
while __Enum2
is predefined but not exposed to rustaceans:
#[derive(Exchange)]
enum __Enum2 {
_0(i32),
_1(&'static str),
}
Two Enum!()
s with identical variant type list are identical types.
<Enum!( T0, T1, .. )>::_0
for the first variant in pattern matching, and so
forth.
match info {
<Enum!(i32,&'static str)>::_0(i) => (),
<Enum!(i32,&'static str)>::_1(s) => (),
}
Predefined enums are also Exchange
able enums, being the destinations of
from_variant()
/into_enum()
, and having exchange_from()
/exchange_into()
methods. By now, we call these four methods as “enum exchange methods”.
Predefined enums are superior as notations, comparing to user-defined ones. From now on, we will prefer using predefined enums in examples.
Convertion rules
The following 2 rules are considered as the minimum support of structural enum:
- variant <=> exchangeable enum
from_variant()
/into_enum()
serve for it.
- exchangeable enum => exchangeable enum composed of equal or more variants
exchange_from()
/exchange_into()
serve for it.
The following rules are considered perculiar to exchangeable enums, which distinguish them from “union types” in Typed Racket.
- Uniqueness of variant type requirement in monomorphisation
An exchangeable enum composed of duplicated variant types is a valid type. However, a compiler error will occur on its instantiation via enum exchange methods:
9 | let a = <Enum!(i32,i32)>::_0( 3722 );
| ^^^^^^^^^^^^^^^^ variants of an exchangeable enum must be unique.
- No automatic flattening
For example, Enum!(A,Enum!(B,C))
can not be converted to Enum!(A,B,C)
via
enum exchange methods. Further more, making these two equal types will need
changes in type systems, which is not possible for a proc-macro derive.
Reference-level explanation
The definition of enum exchange traits are
pub trait FromVariant<Variant,Index> {
fn from_variant( variant: Variant ) -> Self;
}
pub trait IntoEnum<Enum,Index> {
fn into_enum( self ) -> Enum;
}
pub trait ExchangeFrom<Src,Indices> {
fn exchange_from( src: Src ) -> Self;
}
pub trait ExchangeInto<Dest,Indices> {
fn exchange_into( self ) -> Dest;
}
Notice that all traits have a phantom index type Index
or Indices
in their
generics to hold positional information to help compiler accomplishing type
inference.
Distinguish from std::convert
Since standard From
/Into
does not have such index type, it is not feasible
to implement enum exchange methods in From
/Into
. On the other hand, trying
to implement in From
will cause compile error because we need to impl multiple
From<Variant>
but the generic Variant
type in different impl
s could be of
the same actual type, resulting in overlap impl
s.
Distinguish between Index
and Indices
Consider the convertion from Enum!(A,B)
to Enum!(A,B,C,Enum!(A,B))
.
Since flattening is not supported by enum exchange, there are two possible ways for this convertion:
-
making the former as the forth variant of the latter.
-
matching the former to get an
A
orB
, then making it as the first or second variant of the latter.
This is the root cause we distinguish between FromVariant
and ExchangeFrom
.
Interaction with other feature
The library implementation of enum exchange, aka EnumX, is a proof that the proposed construct will left Rust syntax and its type systems as untouched.
Enum variant types, if available, will relax the “newtype limit” in user-defined exchangeable enum.
Drawbacks
Increasing the size of std library.
Rationale and alternatives
Two alternatives to supporting structural enum:
- Making it a native support type, aka “sum type”.
As mentioned previously, it might be too big changes for Rust community to agree on, and may bring lots of work.
- Implementing it as a third party library.
While the using of structural enums was more adopted as it ought to be, a third library mimicing such language features will suffer from leaking implementation details or unfriendly compile errors, both of which confusing the programmers.
Comparson to using trait object
Issues such as fat interface, down-casting and unnecesseary 'static
lifetime
bounds, have been dicussed for using trait object, in motivation chapter.
To solve fat interface problem, traits should be splitted into categories and linked using “trait Concrete: Abstract” syntax.
To address difficulties in down-casting, the trait as the root of hierarchy should provide methods for explicit convertion and force to implement.
The unnecessary 'static
lifetime bounds seems infeasible to resolve for trait
object because the actual types have been erased.
These three are all non-issues for structural enums.
While using trait objects will make programmers to design the hierarchy of traits, using enums does not mean to force programmers not to implement necessary traits. It just provides a different but also fine grain control. These trait bounds could be applied to certain variants rather than the enum.
It is a mistake to force rustaceans to design a hierarchy of traits, just to reflect the shape of hierarchy of functions. Using structural enums is a natural solution in such cases, without all these pitalls mentioned above.
Prior art
Concepts similar with structural enum exist in other language. One example is
union types in Typed Racket. However, it supports more powerful type inferences
such as Enum!(A)
=> A
, Enum!(A,A)
=> Enum!(A)
, Enum!(A,Enum!(B,C))
=>
Enum!(A,B,C)
. All these seems to bring significant changes to Rust’s interals.
Unresolved questions
A particular use case, in which structural enums are collecting variants
implementing the same trait and act if the enums themselves had implement it,
may require more elegant solution, than macro-generated impl
s.
Future possibilities
It seems to be a compatible, fine grained, and practical way to introducing
separate derives. Other proc-macro derives may be proposed. For example, to
support automatic flattening or variant type duplication, a #[derive(Sum)]
could be introduced in the future.
———————————————
The following is the origin proposal.
———————————————
The features suggested here have been implemented as a Rust crate, enumx, and ready for use with stable Rust.
It is a separation from a previous thread, Checked exception simulation in Rust, but focusing on the topic of structural enum types.
Summary
Add types, traits, macros for providing structural enum types in Rust.
The new constructs are:
-
An
Enum!(T0,T1..)
macro to define a structrual enum type composed of variant types T0,T1… etc. -
A set of predefined
enum
types with the namesEnum0
,Enum1
,… etc to notate the structural enums of different amount of variants. -
An
Exchange
trait for deriving user-defined sructuralenum
types.
Motivation
By definition, structural enum types can be converted to each other if their variant types are compatible. If provided, they will help programmers to directly express a sum type concept and keep them from hand-writing or macro-generating boilerplate code for convertion impl
s.
One of an important use cases, putting error types in function signatures, aka throws
, will be discussed in its own thread.
Overview
Definition of structrual enum type
A structural enum type of n variant types is notated as Enum!( T0, T1, .., T(n-1) )
, the actual type name of which is Enum$n<T0,T1,..,T(n-1)>
, while $n
means the digit number n appended to Enum
as a complete identity. Its variants are named as _0
, _1
, … _(n-1)
. The first variant can be pattern matched as <Enum!( T0, T1, .., T(n-1) )>::_0
or Enum$n::_0
, and so forth.
For example, Enum!( i32, &'static str )
is a structural enum type of 2 variants, which is essentially Enum2<i32,&'static str>
. Two of its variants are Enum2::_0
and Enum2::_1
.
Enum0
is also defined as a never type.
Construct a structrual enum
It is obvious that any variant can be converted to the structral enum type containing it.
- Type annotation
let enum2: Enum!(i32,&'static str) = 42.into_enum();
- Type inference
let enum2 = <Enum!(i32,&'static str)>::from_variant( 42 );
Convertion between structrual enums
A structural enum could be converted to another one, if all its variants are in the latter.
For example, Enum!(i32,&'static str)
could be converted to Enum!(&'static str,i32)
, Enum!(i32,bool,&'static str)
, but not Enum!(u32,&'static str)
nor Enum!(i32,String)
.
- Type annotation
let enum2: Enum!(i32,&static str) = 42.into_enum();
let enum2: Enum!(&'static str,i32) = enum2.into_enumx();
let enum3: Enum!(i32,bool,&'static str) = enum2.into_enumx();
- Type inference
let enum2 = <Enum!(i32,&static str)>::from_variant( 42 );
let enum2 = <Enum!(&'static str,i32)::from_enumx( enum2 );
let enum3 = <Enum!(i32,bool,&'static str)::from_enumx( enum2 );
Define a user-defined structrual enum
A user-defined structrual enum should be tagged with #[derive(Exchange)]
, and is able to be exchanged with others of compatible variant types.
To distinguish a user-defined structrual enum from Enum!()
, , by now, we call the former “exchangeable enum”, and the latter “ad-hoc enum”.
#[derive(Exchange)]
enum Info {
Code(i32),
Text(&'static str),
}
#[derive(Exchange)]
enum Data {
Code(i32),
Text(&'static str),
Flag(bool),
}
#[derive(Exchange)]
enum Datum {
Code(u32),
Text(String),
Flag(bool),
}
Construct a exchangeable enum
It is obvious that any variant can be converted to the exchangeable enum containing it.
- Type annotation
let info: Info = 0xdeadbeef.into_enum();
- Type inference
let info = Info::from_variant( 0xdeadbeef );
Convertion between exchangeable enums
A exchangeable enum could be converted to another one, if all its variants are in the latter.
For example, Info
could be converted to Data
, but not Datum
.
- Type annotation
let info: Info = 0xdeadbeaf.into_enum();
let data: Data = info.exchange_into();
- Type inference
let info = Info::from_variant( 0xdeadbeaf );
let data = Data::exchange_from( info );
Detailed Design
Predefined ad-hoc enum types
A set of predefined enum
types with the names Enum0
, Enum1
,… are defined in the form of:
pub enum Enum0 {}
pub enum Enum1<T0> { _0(T0) }
pub enum Enum2<T0,T1> { _0(T0), _1(T1) }
/* omitted */
Translating Enum!()
to the actual ad-hoc enum type
The purpose for Enum!()
is to keep programmers from manually counting variants to pick the right number as a suffix to Enum
.
It could be implemented as a declarative macro, counting its arguments to find the corresponding predefined enum type:
macro_rules! Enum {
( $t0:ty ) => { Enum1<$t0> };
( $t0:ty, $t1:ty ) => { Enum2<$t0,$t1> };
( $t0:ty, $t1:ty, $t2:ty ) => { Enum3<$t0,$t1,$t2> };
/* omitted */
}
Type convertion implementations NOT to use From
This is the most interesting part in implementation. In general, we cannot generate such implementations by implementing std::convert::From
trait, due to the potential overlapping of impl
s, which is not allowed in Rust. Simple demonstration of convertion between two Enum2
s:
impl<T0,T1,U0,U1> From<Enum2<T0,T1>> for Enum2<U0,U1> { /* omitted */ }
If T0 equals to U0 and T1 equals to U1, we are doing impl From<T> for T
now, which will result in compiler error. Further more, we are going to meet this again and again no matter what tricks we play as long as sticking in impl From
for generic types.
A sound method is developing our own traits rather than using standard From
trait to do the convertion. These traits should be able to encode structural information to map variant(s) to its(their) proper positions.
What we need to do is to check the equality of types in trait bounds, which is not directly supported in Rust. We should do some transformations to make rustc happy.
The phantom index
We will introduce a ZST named Nil
, a set of index types which are ZSTs and named as V0
,V1
,…, V(n-1)
to reflect the position of the type list T0,T1,..,T(n-1)
, and a recursive struct pub struct LR<L,R>( pub L, pub R );
, to transform the positions as a “cons of car/cdr”: LR(V0, LR(V1, .. LR(V(n-1),Nil))..)
.
Construct a structrual enum
We will introduce two traits: FromVariant<Variant,Index>
and IntoEnum<Enum,Index>
:
pub trait FromVariant<Variant,Index> {
fn from_variant( variant: Variant ) -> Self;
}
pub trait IntoEnum<Enum,Index> {
fn into_enum( self ) -> Enum;
}
And a blanket impl
:
impl<Enum,Variant,Index> IntoEnum<Enum,Index> for Variant
where Enum: FromVariant<Variant,Index>
{
fn into_enum( self ) -> Enum { FromVariant::<Variant,Index>::from_variant( self )}
}
Mapping a variant to the enum can be done in a declarative macro.
Convertion between structrual enums
We will introduce two traits: IntoEnumX<Dest,Indices>
and FromEnumX<Src,Indices>
:
pub trait IntoEnumX<Dest,Indices> {
fn into_enumx( self ) -> Dest;
}
pub trait FromEnumX<Src,Indices> {
fn from_enumx( src: Src ) -> Self;
}
And a blanket impl
:
impl<Src,Dest,Indices> FromEnumX<Src,Indices> for Dest
where Src: IntoEnumX<Dest,Indices>
{
fn from_enumx( src: Src ) -> Self { src.into_enumx() }
}
For demonstrating the key idea, the following code snippet is quoted from EnumX:
impl<L,R,T0,$($descent_generics),+,$($dest_generics),+> IntoEnumX<$dest_enum<$($dest_generics),+>,LR<L,R>> for $src_enum<T0,$($descent_generics),+>
where $dest_enum<$($dest_generics),+> : FromVariant<T0,L>
, $descent_enum<$($descent_generics),+> : IntoEnumX<$dest_enum<$($dest_generics),+>,R>
T0
is the first variant type of the source enum.
What we are doing is essentially check if the dest enum can be constructed from T0
, and if not, try converting the rest variant types in source enum into the dest.
The L
is the first index and the R
is the rest indices. Notice: the two where clauses are not possible to be true both with non-Nil
at the same time.
Define a user-defined structrual enum
We will introduce an Exchange
trait to reflect the prototype of an exchangeable enum, that is, an ad-hoc enum of the same variant types but renaming the variant names as _0
,_1
,… accordingly.
pub trait Exchange {
type EnumX;
}
For example, the Exchange::EnumX
of the Info
defined in previous section is Enum2<i32,&'static str>
.
Construct an exchangeable enum
It is obvious for #[derive(Exchange)]
to generate impl Exchange
, impl From
EnumX, impl Into
EnumX. All that we need to do is naming/renaming.
To impl FromVariant<Variant,Index> for ExchangeableEnum
, first convert the variant to Exchange::EnumX
, then convert it Into
ExchangeableEnum.
To impl IntoEnumX<AdhocEnum,Indices> for ExchangeableEnum
, first convert the ExchangeableEnum Into
Exchange::EnumX
, then convert it IntoEnumX
AdhocEnum.
Convertion between exchangeable enums
We will introduce two traits: ExchangeFrom<Src,Indices>
and ExchangeInto<Dest,Indices>
:
pub trait ExchangeFrom<Src,Indices> {
fn exchange_from( src: Src ) -> Self;
}
pub trait ExchangeInto<Dest,Indices> {
fn exchange_into( self ) -> Dest;
}
We convert the source enum to its ad-hoc enum, then to the dest’s ad-hoc enum, then to the dest enum.
impl<Src,SrcAdhoc,Dest,DestAdhoc,Indices> ExchangeFrom<Src,Indices> for Dest
where Dest : Exchange<EnumX=DestAdhoc> + From<DestAdhoc>
, Src : Exchange<EnumX=SrcAdhoc> + Into<SrcAdhoc>
, DestAdhoc : FromEnumX<SrcAdhoc,Indices>
{
fn exchange_from( src: Src ) -> Self {
Dest::from( FromEnumX::<SrcAdhoc,Indices>::from_enumx( src.into() ))
}
}
And a blanket trait implementation.
impl<Src,Dest,Indices> ExchangeInto<Dest,Indices> for Src
where Dest: ExchangeFrom<Src,Indices>
{
fn exchange_into( self ) -> Dest {
Dest::exchange_from( self )
}
}
Drawbacks
-
The various kind of
From
/Into
-alike traits may confuse users, and losing the chance in the situation that accepts standardFrom
/Into
only. -
Predefined ad-hoc enums are a subset of possible ad-hoc enums. What if the programmer want an ad-hoc enum composed of 65535 variants?
Rationale and alternatives
-
The EnumX v0.2 is inspired by
frunk_core::coproduct
, which provides another ad-hoc enum implementation. It uses the recursive enum as a public interface, getting rid of the variants count limit, at the cost of not supporting native pattern matching syntax onenum
s. And the enum in recursive form may occupy more spaces than the flattern one proposed by this article. -
The EnumX v0.1 generates the convertion in standard
From
/Into
trait, at the cost of not supporting generics, and heavy hacks in proc-macro attribute#[enumx]
.
Prior art
frunk_core::coproduct
, and EnumX
v0.1 just mentioned.
Unresolved questions
Exchangeable enum is missing FromEnumX
in the derive. As a result, there is no way to convert an ad-hoc enum to an exchangeable one automatically.
Future possibilities
Make exchangeable enum a first-class structrual enum type.