Introduction to enumx

This project provides ENUM eXtensions to simulate the following features:

  • "Union types" in Racket, with the special interest in error-handling, aka checked exception.

  • summaries into an enum, the returned values of different types by functions that return impl Trait.

  • macros to help implementing traits for enums the variants of which have all implemented the traits.

Four crates categorized into the fowllowing sub projects:

EnumX, the enum extension library.

Type/trait definitions in enumx crate and proc macros in enumx_derive crate.

CeX, for Checked EXception.

Type/trait definitions in cex crate and proc macros in cex_derive crate.

Ad-hoc enum types

A Rust tuple could be considered as an ad-hoc struct for which the programmers do not need to name the type nor the fields. As an analogy, an ad-hoc enum is implicitly defined by its variants.

Unfortunately Rust does not support ad-hoc enums. This library uses Enum!() macros for simulation. For instance, the definition of Enum!(A,B,C) is as follows:


#![allow(unused)]
fn main() {
enum Enum3<A,B,C> {
    _0( A ),
    _1( B ),
    _2( C ),
}
}

The Enum!() macro denotes a series of generic enums named Enum0, Enum1, Enum2, .. which composed of 0,1,2.. variants. These enums should be defined beforehand, either predefined in this library, or defined by the library users.

Predefined ad-hoc enums

This library has defined Enum0, Enum1 .. up to Enum16 by default.

The library user can use enumx::predefined::*; for convenience.

A feature named "enum32" increases the set of predefined enums up to Enum32.

Cargo.toml:

[dependencies.enumx]
version = "0.4"
features = "enum32"

The predefined enums can be disabled by opting out "Enum16" and "Enum32" features.

Cargo.toml:

[dependencies.enumx]
version = "0.4"
default-features = false

User defined ad-hoc enums

Sometimes the library users have to define these ad-hoc enums themselves to implement more traits which are not implemented for predefined enums in this library.


#![allow(unused)]
fn main() {
use enumx::export::*;

def_impls! {
    #[derive( SomeTraitNeverHeardByEnumxAuthor )]
    enum Enum![ 0..=16 ];
}
}

Enum![ 0..=16 ] means Enum0,Enum1,.. up to Enum16. The name Enum can be replaced by any valid identity. For instance, MyEnum![ 1..=6 ] means MyEnum1, MyEnum2, up to MyEnum6.

Where clause is supported by def_impls!{}.


#![allow(unused)]
fn main() {
    use enumx::export::*;

    def_impls! {
        pub enum Enum![ 0..=16 ]
            where _Variants!(): Iterator<Item=i32>;
    }
}

impl traits for enums

It is very common to implement traits for enums if all of its variants have implemented the traits.


#![allow(unused)]
fn main() {
enum Data {
    Bin( Vec<u8> ),
    Text( String ),
}

impl AsRef<[u8]> for Data {
    fn as_ref( &self ) -> &[u8] {
        match self {
            Data::Bin(  v ) => v.as_ref(),
            Data::Text( v ) => v.as_ref(),
        }
    }
}
}

In case of generic enums:


#![allow(unused)]
fn main() {
impl<T0,T1> AsRef<[u8]> for Enum2<T0,T1>
    where T0: AsRef<[u8]> 
        , T1: AsRef<[u8]> 
{
    fn as_ref( &self ) -> &[u8] {
        match self {
            Enum2::_0( s ) => s.as_ref(),
            Enum2::_1( s ) => s.as_ref(),
        }
    }
}
}

The basic idea is to match the variants and delegate. This library provides macros to help to avoid writing these boilerplate code:

  1. The contextual macros

These macros helps not repeating the where clauses and match arms of each variant.

  1. The predefined macros

These macros helps to omit the methods in impl blocks.

The contextual macros

This library introduces a proc macro def_impls!{} in which you can define an enum and write several impl blocks. In these impl blocks, the following macros are supported:

  1. _Variants!() in where clause. For example, where _Variants!(): SomeTrait means each variant has the trait bound of : SomeTrait.

  2. _match!() in trait method. This macro will expand to a match expression that enumerate all variants, and translate the macros listed below.

  3. _variant!() in _match!(). This macro will expand to the value of the matched variant.

  4. _Variant!() in _match!(). This macro will expand to the type of the matched variant.

  5. _enum!() in _match!(). This macro will wrap its inner value to get an enum. Use this macro if the trait method returns Self.

Examples

Let's rewrite the generic enum example:


#![allow(unused)]
fn main() {
def_impls! {
    impl<T0,T1> AsRef<[u8]> for Enum2<T0,T1>
        where _Variants!(): AsRef<[u8]> 
    {
        fn as_ref( &self ) -> &[u8] {
            _match!(
                _variant!().as_ref()
            )
        }
    }
}
}

Another example, implementing Clone. Note the using of _enum!().


#![allow(unused)]
fn main() {
def_impls! {
    impl<T0,T1> Clone for Enum2<T0,T1>
        where _Variants!(): Clone
    {
        fn clone( &self ) -> Self {
            _match!(
                _enum!( _variant!().clone() )
            )
        }
    }
}
}

You can specify the expression matched being matched, using the syntax _match!( matched => expr ). If ommited, _match!( expr ) is equivalent to _match!( self => expr ).


#![allow(unused)]
fn main() {
def_impls! {
    impl<T0, T1, R, Yield, Return> std::ops::Generator<R> for Enum2<T0,T1>
        where _Variants!(): std::ops::Generator<R,Yield=Yield,Return=Return>
    {
        type Yield = Yield;
        type Return = Return;
        fn resume( self: std::pin::Pin<&mut Self>, arg: R )
            -> std::ops::GeneratorState<Self::Yield, Self::Return>
        {
            _match!( unsafe{ self.get_unchecked_mut() } =>
                 unsafe{ std::pin::Pin::new_unchecked( _variant!() )}
                    .resume( arg )
            )
        }
    }
}
}

The predfined macros

For frequently used traits in std, this library provides macros such as impl_trait!() to implement these traits without the need of writing trait methods.

Syntax of impl_trait!{}

The full form is


#![allow(unused)]
fn main() {
impl_trait! {
    _impl!( Generics ) Path::Of::Trait _for!( Type ) _where!( Clause )
}
}

Generics and Clause are optional:


#![allow(unused)]
fn main() {
impl_trait!{ _impl!() Path::Of::Trait _for!( Type ) _where!() }
}

and the wrapped macros can be omitted:


#![allow(unused)]
fn main() {
impl_trait!{ Path::Of::Trait _for!( Type )}
}

Supported forms of types in _for!()

The _for!() macro supports two forms of types.

One is ad-hoc enums:


#![allow(unused)]
fn main() {
impl_trait!{ Path::Of::Trait _for!( Enum![1..=16] )}
}

The other is the enum type definition copied in _def!() macro:


#![allow(unused)]
fn main() {
impl_trait!{ Path::Of::Trait _for!( _def!(
    enum Value {
        Bin( Vec<u8> ),
        Text( String ),
    }
))}
}

Note: _def!() does not define any enum, so Value should have been defined elsewhere.

The _where!() macro

You can write any where clause in this macro.

Note: you do not need write _where!( _Variants!(): Path::Of::Trait ) which the impl_trait!{} macro will generate it silently.

Traits in std prelude

AsRef

AsMut

DoubleEndedIterator

ExactSizeIterator

Extend

Fn

Iterator

The example of implementing Iterator:


#![allow(unused)]
fn main() {
impl_trait!{ Iterator _for!( Type )}
}

The example of implementing Fn:


#![allow(unused)]
fn main() {
impl_trait!{ _impl!(Args) Fn<Args> _for!( Type )}
}

Traits with full path

std::error::Error

std::fmt::Debug

std::fmt::Display

std::iter::FusedIterator

std::iter::TrustedLen

std::io::BufRead

std::io::Read

std::io::Seek

std::io::Write

std::ops::Deref

std::ops::DerefMut

std::ops::Generator

std::ops::Index

std::ops::IndexMut

std::ops::RangeBounds

The example of implementing std::ops::Generator:


#![allow(unused)]
fn main() {
impl_trait!{ _impl!(R) std::ops::Generator<R> _for!( Type )}
}

Unstable traits

To implement these traits, the crate feature "unstable" should be opted in.

Fn

std::iter::TrustedLen

std::ops::Generator

impl_super_traits!{} and impl_all_traits!{}

The syntax of these two traits are similar with impl_trait!{}.

The impl_super_traits!{} macro helps to implement the super trait(s) of the mentioned trait, e.g. impl_super_traits!{ _impl!(Args) Fn<Args> _for!( Type )} will implement FnMut and FnOnce for Type, but NOT Fn.

The impl_all_traits!{} macro does what impl_trait!{} and impl_super_traits!{} does, e.g. impl_all_traits!{ _impl!(Args) Fn<Args> _for!( Type )} will implement Fn, FnMut and FnOnce for Type.

macro inheritance

If the library users want to support extra traits, they can write the extra implementations in their macro, and delegate other traits to enumx::impl_trait!().


#![allow(unused)]
fn main() {
use enumx::export::{def_impls, impl_all_traits};

macro_rules! impl_trait {
    ($(_impl!($($gen:tt),*))* ExtraTrait<$t:ident> _for!($($ty:tt)+) $(_where!($($pred:tt)*))*) => {
        // omitted
    };
    ($($tt:tt)+) => {
        enumx::impl_trait!{ $($tt)+ }
    };
}


macro_rules! impl_super_traits {
    ($(_impl!($($gen:tt),*))* ExtraTrait<$t:ident> _for!($($ty:tt)+) $(_where!($($pred:tt)*))*) => {
        // omitted
    };
    ($($tt:tt)+) => {
        enumx::impl_super_traits!{ $($tt)+ }
    };
}
}

Sum impl Trait

This is an extension to allow multiple return types which implement the same trait.

Example


#![allow(unused)]
fn main() {
#[sum]
fn f( cond: bool ) -> impl Clone {
    if cond {
        #[variant] 1_i32
    } else {
        #[variant] "false"
    }
}
}

Enums external to function

In previous example, the #[sum] tag will generate an enum type local to the function. An anternative way is to use externally defined enums, for which the library users can implement traits manually.


#![allow(unused)]
fn main() {
use serde::{Serialize, Serializer};

use enumx::export::*;
// DO NOT use enumx::predefined::*;

def_impls! {
    enum Enum![2..=3];

    impl Serialize for Enum![2..=3]
        where _Variants!(): Serialize
    {
        fn serialize<S: Serializer>( &self, serializer: S ) -> Result<S::Ok, S::Error> {
            _match!( _variant!().serialize( serializer ))
        }
    }
}

#[sum( Enum )]
fn f( cond: bool ) -> impl Serialize {
    if cond {
        #[variant] 1_i32
    } else {
        #[variant] "false"
    }
}

#[sum( Enum )]
fn g( cond: u32 ) -> impl Serialize {
    match cond % 3 {
        0 => #[variant] 1_i32,
        1 => #[variant] "false",
        2 => #[variant] true,
        _ => unreachable!(),
    }
}
}

Trait annotation

The #[sum] tag will analyze the function's return type and decide which impl Trait to summarize. If it is not what you want, use #[sum( impl Trait )] to annotates the impl Trait explicitly.

If both trait annotation and externally defined enum type are required, use #[sum( impl Trait for Enum )].

The variant attribute

The #[sum] tag collects all expressions with #[variant] attributes and wraps them with enum constructors in the form of SomeEnumName::_0, SomeEnumName::_1.. respectively. For example, the function body in previous example will be expanded to:


#![allow(unused)]
fn main() {
if cond {
    __SumType2::_0( 1_i32 )
} else {
    __SumType2::_1( "false" )
}
}

Merge variants of the same types

The #[variant] attribute supports merging by giving the same name of merged variants. For example, a series of expresions with #[variant( foo )], #[variant], #[variant( foo )] will be wrapped with _0, _1, _0.

Multiple sum tags

This library supports to tag a function with multiple #[sum] attributes, which summarize different impl Trait into different enums.

More syntax of #[sum] and #[variant]

  1. #[sum( sum_name => impl Trait )]

  2. #[sum( sum_name => impl Trait for Enum )]

  3. #[variant( sum_name => variant_name )]

  4. #[variant( sum_name => _ )]

The sum_name tells which impl Trait enum the #[sum]/#[variant] belongs to.


#![allow(unused)]
fn main() {
#[sum( ok  => impl Clone )]
#[sum( err => impl Clone )]
fn sum_okeys_and_errors( branch: i32 ) -> Result<impl Clone, impl Clone> {
    match branch % 4 {
        0 => Ok(  #[variant( ok  => _ )] branch ),
        1 => Ok(  #[variant( ok  => _ )] () ),
        2 => Err( #[variant( err => _ )] branch ),
        3 => Err( #[variant( err => _ )] () ),
        _ => unreachable!(),
    }
}
}

Support of ?

This library introduce a proc-macro attribute named #[sum_err], to translate the expr? expressions in a different manner than the Rust's default:


#![allow(unused)]
fn main() {
match expr {
    Ok( value ) => value,
    Err( error ) => return Err( #[variant] error ),
}
}

A #[sum] tagged function should be tagged with #[sum_err] if it contains ? expressions.

Example


#![allow(unused)]
fn main() {
#[sum_err]
#[sum( impl Clone )]
fn foo( branch: i32 ) -> Result<(), impl Clone> {
    match branch % 3 {
        0 => Ok(()),
        1 => Ok( Err( 0 )? ),
        2 => Ok( Err( "lorum" )? ),
        _ => unreachable!(),
    }
}
}

Note: put #[sum_err] before #[sum].

Enum exchange

Suppose Enum!(A,B,C,..) denotes a type similar to enum that composed of variants A,B,C.., with extra features:

  1. Variants of the duplicated type are merged. For instance, Enum!(A,B,A) is Enum!(A,B).

  2. Order of variants does not matter. For instance, Enum!(A,B) is Enum!(B,A).

  3. Enum!()s as variants are flattened. For instance, Enum!(A, Enum!(B,C)) is Enum!(A,B,C).

  4. Any subset of an Enum!() can be converted to it. For instance, A, Enum!(A) and Enum!(A,B) can be converted to Enum!(A,B,C).

Such types, which are similar to Racket's "union types", do not exist in Rust's type systems. With the help of this library, the best we can get is "union values":

  1. Enum!()s that has duplicated variant types cannot be converted to each other without extra annotation, which is not practicable.

  2. Two Enum!()s composed of the same variant types but with different order can be converted to each other. For instance, Enum!(A,B) can be converted to Enum!(B,A), and vise vesa.

  3. Enum!()s as variants are not flattened in conversion. This library might support converting Enum!(A,C) to Enum!(A, Enum!(B,C)) in the future( perhaps if Rust supports where T != U ), but not now.

  4. Any subset of an Enum!() can be converted to it.

This library names the conversion in #2 and #4 as "enum exchange", and defines an derivable Exchange trait.

The Exchange trait

This library provides ExchangeFrom/ExchangeInto traits which is similar to std From/Into but with extra phantom generic type.


#![allow(unused)]
fn main() {
pub trait ExchangeFrom<Src, Index> {
    fn exchange_from( src: Src ) -> Self;
}

pub trait ExchangeInto<Dest, Index> {
    fn exchange_into( self ) -> Dest;
}

}

Blanket implementations of ExchangeInto are similar to Into:


#![allow(unused)]
fn main() {
impl<Src, Dest, Index> ExchangeInto<Dest, Index> for Src
    where Dest: ExchangeFrom<Src, Index>,
{
    fn exchange_into( self ) -> Dest {
        Dest::exchange_from( self )
    }
}
}

Any enum in the form described below can derive these traits automatically, by using #[derive( Exchange )].


#![allow(unused)]
fn main() {
use enumx::export::*;

#[derive( Exchange )]
enum Data {
    Bin( Vec<u8> ),
    Text( String ),
}

#[derive( Exchange )]
enum Value {
    Bin( Vec<u8> ),
    Text( String ),
    Literial( &'static str ),
}

// use ExchangeFrom
let data = Data::exchange_from( "foo".to_owned() );
let value = Value::exchange_from( data );

// use ExchangeInto
let data: Data = "foo".to_owned().exchange_into();
let value: Value = data.exchange_into();
}

This library provides predefined enums that have implement ExchangeFrom/ExchangeInto. The user can use enumx::predefined::*;, and use Enum!() macro to denote types, as described in Ad-hoc enum types.

Alternatively, the user is able to define their own ad-hoc enum types:


#![allow(unused)]
fn main() {
use enumx::export::*;
// do not use enumx::predefined::*;

def_impls! {
    #[derive( Exchange )]
    pub enum Enum![ 1..=16 ];
}
}

Type as Pattern

The variant names of ad-hoc enums are not so attractive: _0,_1,_2..etc. There are some issues when these names are used in match expressions:

  1. They are ugly and meaningless. The numbers do not reflect the types.

  2. Subject to changes of ad-hoc enum. For instance, changing Enum!(Alpha,Gamma) to Enum!(Alpha,Beta,Gamma) will break the arms matching _1.

This library provides a feature so called "type as pattern", which extends the syntax of match expressions to accept variant type names in arm's pattern.

Use #[ty_pat] match

to do pattern matching against an Enum!(A, B, ..), the arms of which are not variants but types A, B, .. etc. The fn containing the match expression must be tagged #[enumx].


#![allow(unused)]
fn main() {
#[enumx] fn foo( input: Enum!(String,i32) ) {
    #[ty_pat] match input {
        String(s) => println!( "it's string:{}", s ),
        i32(i) => println!( "it's i32:{}", i ),
    }
}
}

Use #[ty_pat(gen_variants)]

to generate missing types in Enum!():


#![allow(unused)]
fn main() {
#[enumx] fn foo( input: Enum!(String,i32) ) -> Enum!(String,i32) {
    #[ty_pat(gen_variants)] match input {
        i32(i) => (i+1).exchange_into(),
        // generated arm: String(s) => s.exchange_into(),
    }
}
}

Use #[ty_pat(gen A,B,..)]

to generate A,B,.. etc:


#![allow(unused)]
fn main() {
#[enumx] fn foo( input: Enum!(String,i32) ) -> Enum!(String,i32) {
    #[ty_pat(gen String)] match input {
        i32(i) => (i+1).exchange_into(),
        // generated arm: String(s) => s.exchange_into(),
    }
}
}

Use TyPat

to wrap types that are not paths, e.g. references, (), in a #[ty_pat] match's arm:


#![allow(unused)]
fn main() {
#[enumx] fn bar( input: Enum!(&'static str,i32) ) {
    #[ty_pat] match input {
        TyPat::<&'static str>(s) => println!( "it's static str:{}", s ),
        i32(i) => println!( "it's i32:{}", i ),
    }
}
}

Checked exception

This library simulates checked exception by enumerating in function's signatures every possible error types in ad-hoc enums which are Exchange-able and matched their variants' types as patterns.

Usage

Add this crate to Cargo.toml, and enable any feature you want

Cargo.toml:

enumx = "0.4"
cex = "0.5"

src/lib.rs:


#![allow(unused)]
fn main() {
use enumx::export::*;
use enumx::predefined::*; // or use your own enum types at your will.
use cex::*;
}

Extended syntax:

  1. #[cex] proc macro attribute for functions/closures/let-bindings returning checked exceptions.

  2. Result!() annotates the return type.

  3. ret!()/throw!() for control flow.

  4. #[ty_pat] for "type as pattern"

Result!() macro

The syntax of Result!() macro is Result!( OkType throws Err1, Err2, .. ), the underlying type of which is Result<OkType, Enum!(Err1, Err2, ..)>. However the Result!() macro is preferred over Enum!() because:

  1. Enum!() is subject to changes on feature of log/env_log, while Result!() is not.

  2. throws is cool, shorter and more clear than Enum!().

Use Result!() to enumerate the possible error types

  • in function signature:

#![allow(unused)]
fn main() {
#[cex] fn throws_never() -> Result!(i32) {/**/}

struct SomeError;

#[cex] fn foo() -> Result!( i32 throws String, &'static str, SomeError ) {/**/}
}
  • in closure's signature:

#![allow(unused)]
fn main() {
fn foo() {
    let _f = #[cex] || -> Result!( i32 throws String ) {/**/}
}
}
  • in the type annotation of a local let-binding:

#![allow(unused)]
fn main() {
fn foo() {
    #[cex] let v: Result!( i32 throws String ) = try {/**/};
}
}

The ret!()/throw!() macros

The underlying control flow constructs of these two macros are return. However, ret!()/throw!() macros are preferred over return because:

  1. Using return is subject to changes on feature of log/env_log, while using ret!()/throw!() are not.

  2. ret!()/throw!() are cool and more clear than return.

  3. ret!() supports Ok-wrapping.

The syntax of ret!()

  1. ret!( ok_value ), or

  2. ret!( result_value )

In other words, you can use ret!() to return an Ok expression:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws String ) {
    ret!( 42 ); // Ok-wrapping
}
}

or you can use ret!() to return a Result expression:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws String ) {
    ret!( Ok( 42 ));
    // or ret!( Err( String::from( "oops" )))
}
}

The syntax of throw!()

is throws!( err_value ).

You can use throw!() to return an Err expression:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws String, SomeError ) {
    throw!( String::from( "oops" ))
    // or throw!( SomeError )
}
}

Thanks to the power of Exchange:


#![allow(unused)]
fn main() {
#[cex] fn bar() -> Result!( i32 throws String, &'static str, SomeError ) {
    match foo() {
        Ok(v) => ret!(v),
        Err(e) => throw!(e), // all errors in foo()'s throws are in bar()'s
    }
}
}

Thanks to the power of ? which looks like throwing checked exceptions:


#![allow(unused)]
fn main() {
// equivalent to bar()
#[cex] fn baz() -> Result!( i32 throws String, &'static str, SomeError ) {
    ret!( foo()? ) // of course you can use `?` to propagate errors
}
}

Backtrace

Backtrace is disabled by default. When enabled, locations of error propagation by ret!(), throw!() and ? operator will be stored in the Err variant.

Use log feature to enable backtrace.

[dependencies.cex]
version = "0.5"
features = ["log"]

Use env_log feature to enable backtrace if the envirnoment variable

RUST_BACKTRACE is 1 or "full".

[dependencies.cex]
version = "0.5"
features = ["env_log"]

Use pretty_log feature to pretty-print the frames, as if "{:#?}" were used.

[dependencies.cex]
version = "0.5"
features = ["log","pretty_log"]
# or features = ["env_log","pretty_log"]
use enumx::export::*;
use enumx::predefined::*;
use cex::*;

#[cex]
pub fn foo() -> Result!( () throws () ) {
    throw!( () );
}

#[cex]
pub fn bar() -> Result!( () throws () ) {
    ret!( foo()? );
}

fn main() {
    bar().unwrap();
}

The output is similar as follows:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: _0(Log {
    error: (),
    agent: [
        Frame {
            module: "my_program",
            file: "src/main.rs",
            line: 5,
            column: 13,
            info: Some(
                "throw!(())",
            ),
        },
        Frame {
            module: "my_program",
            file: "src/main.rs",
            line: 10,
            column: 11,
            info: Some(
                "foo()",
            ),
        },
    ],
})', src/main.rs:14:5

Forward log features to cex crate

[features]
log = ["cex/log"]
env_log = ["cex/env_log"]
pretty_log = ["cex/pretty_log"]

ret!()/throw!() could have the second argument as a customized log item.


#![allow(unused)]
fn main() {
ret!( expr, || frame!( "expect an ok value" ));
throw!( expr, || frame!( "oops" ));
}

Even if backtrace is disabled at compile time, these will compile. The second argument just has no effect.

"Type as Pattern" makes sense for narrowing

Suppose two functions which returns "checked exceptions":


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( () throws A,B );

#[cex] fn bar() -> Result!( () throws A,B,C );
}

If we call foo() in bar(), the errors in foo() may be collected into bar()'s error type Enum!(A,B,C), which is "wider" than foo()'s error type Enum!(A,B). Usually ? could be used for convenience, propagating errors without the need of writing a match expression to handle.

In the contrast, if we call bar() in foo(), it is not possible for ?to propagate the errors from bar() because foo()'s error type is "narrower" than bar()'s. We must write some match expression and will meet the issues.

The #[ty_pat] attribute is enabled inside a #[cex] tagged function or closure, to address these issues and make propagating convenient again.

Use #[ty_pat] match

to map errors returned by #[cex] functions or closures:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( () throws String, SomeError ) {/**/}

#[cex] fn bar() {
    if let Err( err ) = foo() {
        #[ty_pat] match err {
            String( s ) => println!( "foo's error:{}", s ),
            SomeError => println!( "foo's error: SomeError" ),
        }
    }
}
}

Use TyPat

to wrap types that are not paths, e.g. references, (), in a #[ty_pat] match's arm:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws &'static str, SomeError ) {/**/}

#[cex] fn bar() {
    if let Err( err ) = foo() {
        #[ty_pat] match err {
            TyPat::<&'static str>( s ) => println!( "foo's error:{}", s ),
            SomeError => println!( "foo's error: SomeError" ),
        }
    }
}
}

Use #[ty_pat(gen_throws)] match

to automatically propagate errors enumerated in throws:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws String, SomeError ) {/**/}

#[cex] fn bar() -> Result!( i32 throws String ) {
    foo().or_else( |err| #[ty_pat(gen_throws)] match err {
        SomeError => ret!(0),
        // generated arm: String(s) => throw!(s),
    })
}
}

Use #[ty_pat(gen A,B,..)] match

to automatically propagate errors A,B,.. enumerated in the attribute:


#![allow(unused)]
fn main() {
#[cex] fn foo() -> Result!( i32 throws String, SomeError ) {/**/}

#[cex] fn bar() -> Result!( i32 throws String ) {
    foo().or_else( |err| #[ty_pat(gen String)] match err {
        SomeError => ret!(0),
        // generated arm: String(s) => throw!(s),
    })
}
}

Fallback as impl Trait

Sometimes the downstream users do not bother "poluting" signatures of functions which call upstream #[cex] APIs returning checked exceptions. If all variants of the error Enum!() have implemented some famous trait, e.g. std::error::Error, the downstream users get a chance to simply write -> Result<_, impl std::error::Error> in their function signatures.

Example


#![allow(unused)]
fn main() {
use std::error::Error;
use enumx::export::*;
use enumx::predefined::*;
use cex::*;

impl Error for A { /* omitted */ }
impl Error for B { /* omitted */ }
impl Error for C { /* omitted */ }

#[cex] pub fn some_cex_function() -> Result!( () throws A, B, C );

fn downstream() -> Result<(), impl Error> {
    Ok( some_cex_function()? )
}
}

Fallback as "crate error"

The checked exceptions are not necessarily infectious. The library author could provide the crate error type for library users to use in their own functions the error types of which are all from the library.

Example

In upstream crate that adopts checked exceptions:


#![allow(unused)]
fn main() {
use enumx::export::*;
use enumx::predefined::*;
use cex::*;

#[derive( Debug )] // not mandatory
pub struct ErrorA( /* omitted */ );

#[derive( Debug )] // not mandatory
pub struct ErrorB( /* omitted */ );

#[derive( Debug )] // not mandatory
pub struct ErrorC( /* omitted */ );

crate_error!{
    #[derive( Debug )] // not mandatory
    pub enum CrateError {
        ErrorA,
        ErrorB,
        ErrorC,
    }
}

#[doc( hidden )]
pub trait IntoCrateError {
    fn into_crate_error( self ) -> CrateError;
}

impl<E: IntoCrateError> From<E> for CrateError {
    fn from( e: E ) -> Self { e.into_crate_error() }
}

def_impls! {
    // the maximium variant count is 4 in upstream crate.
    impl IntoCrateError for Enum![1..=4]
        where _Variants!(): Into<CrateError>
    {
        fn into_crate_error( self ) -> CrateError {
            _match!(
                _variant!().into()
            )
        }
    }
}

pub type CrateResult<T> = Result<T, CrateError>;

#[cex] pub fn some_cex_function() -> Result!( () throws ErrorA, ErrorB );
#[cex] pub fn another_cex_function() -> Result!( () throws ErrorA, ErrorC );

}

In downstream crate that do not adopt checked exceptions:


#![allow(unused)]
fn main() {
fn downstream() -> upstream::CrateResult<()> {
    some_cex_function()?;
    Ok( another_cex_function()? )
}
}

Fallback as Box<dyn std::error::Error>

Checked exceptions can work with std::error::Error objects as long as all the variants have implemented std::error::Error.

Example

In upstream crate that adopts checked exceptions:


#![allow(unused)]
fn main() {
use enumx::export::*;
use enumx::predefined::*;
use cex::*;

#[derive( Debug )]
pub struct ErrorA( /* omitted */ );
impl_std_error!( ErrorA );

#[derive( Debug )]
pub struct ErrorB( /* omitted */ );
impl_std_error!( ErrorB );

#[cex] pub fn some_cex_function() -> Result!( () throws ErrorA, ErrorB );
}

In downstream crate that do not adopt checked exceptions:


#![allow(unused)]
fn main() {
fn downstream() -> Result<(), Box<dyn std::error::Error>> {
    some_cex_function()?;
    Ok( function_from_other_crate()? )
}
}