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(), } } } }
#![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:
These macros helps not repeating the where clauses and match arms of each variant.
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:
-
_Variants!()
in where clause. For example,where _Variants!(): SomeTrait
means each variant has the trait bound of: SomeTrait
. -
_match!()
in trait method. This macro will expand to a match expression that enumerate all variants, and translate the macros listed below. -
_variant!()
in_match!()
. This macro will expand to the value of the matched variant. -
_Variant!()
in_match!()
. This macro will expand to the type of the matched variant. -
_enum!()
in_match!()
. This macro will wrap its inner value to get an enum. Use this macro if the trait method returnsSelf
.
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]
-
#[sum( sum_name => impl Trait )]
-
#[sum( sum_name => impl Trait for Enum )]
-
#[variant( sum_name => variant_name )]
-
#[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:
-
Variants of the duplicated type are merged. For instance,
Enum!(A,B,A)
isEnum!(A,B)
. -
Order of variants does not matter. For instance,
Enum!(A,B)
isEnum!(B,A)
. -
Enum!()
s as variants are flattened. For instance,Enum!(A, Enum!(B,C))
isEnum!(A,B,C)
. -
Any subset of an
Enum!()
can be converted to it. For instance,A
,Enum!(A)
andEnum!(A,B)
can be converted toEnum!(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":
-
Enum!()
s that has duplicated variant types cannot be converted to each other without extra annotation, which is not practicable. -
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 toEnum!(B,A)
, and vise vesa. -
Enum!()
s as variants are not flattened in conversion. This library might support convertingEnum!(A,C)
toEnum!(A, Enum!(B,C))
in the future( perhaps if Rust supportswhere T != U
), but not now. -
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:
-
They are ugly and meaningless. The numbers do not reflect the types.
-
Subject to changes of ad-hoc enum. For instance, changing
Enum!(Alpha,Gamma)
toEnum!(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:
-
#[cex]
proc macro attribute for functions/closures/let-bindings returning checked exceptions. -
Result!()
annotates the return type. -
ret!()
/throw!()
for control flow. -
#[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:
-
Enum!()
is subject to changes on feature oflog
/env_log
, whileResult!()
is not. -
throws
is cool, shorter and more clear thanEnum!()
.
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:
-
Using
return
is subject to changes on feature oflog
/env_log
, while usingret!()
/throw!()
are not. -
ret!()
/throw!()
are cool and more clear thanreturn
. -
ret!()
supports Ok-wrapping.
The syntax of ret!()
-
ret!( ok_value )
, or -
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()? ) } }