sumtype crate

In Rust, when a function needs to return different types that implement the same trait based on its arguments, it can be challenging because even if the types provide the same interface, they are considered to have different concrete types. This makes it impossible to simply use a return statement to return them directly. A common solution to this problem is to use Box<dyn Trait>, but this approach has two drawbacks: it incurs extra heap memory usage, and it does not provide zero-cost abstraction. Additionally, the types must often have a 'static lifetime.
Here’s an example to illustrate this:
// Example with Iterator trait
// Example with Read trait
In these examples, the functions need to return different concrete types that implement the same trait (Iterator or Read). Using Box<dyn Trait> allows us to return them from the same function, but it introduces the aforementioned drawbacks.
This crate addresses these issues by generating a single anonymous sum type for contexts decorated with the #[sumtype] attribute. By using the sumtype!([expr]) macro, you can wrap the given expression in this sum type. Since the wrapped types become the same type within the same #[sumtype] context, it enables scenarios like returning different types that implement the same trait from a single function. Internally, this sum type uses a simple enum, which means it does not consume additional heap memory. Furthermore, it provides zero-cost abstraction. For instance, in the previous examples, if the conditions can be determined statically by the compiler, the returned types can also be determined, potentially eliminating any additional abstraction cost introduced by the sum type.
Here's how it looks with sumtype:
use sumtype;
// Iterator example
// Read example
In these examples, the #[sumtype] attribute generates a sum type that can wrap different concrete types implementing the specified trait. The sumtype! macro is used to wrap the expressions, allowing them to be treated as the same type within the function. Because the sum type uses a simple enum internally, it avoids additional heap allocations. If the conditions are known at compile time, the compiler can optimize the returned types without incurring extra abstraction costs.
Zero-Cost Abstraction vs Box<dyn Trait>
One of the key advantages of sumtype is its zero-cost abstraction when compared to Box<dyn Trait>.
Benefits of sumtype:
- No heap allocation - Uses stack-allocated enum variants
- Static dispatch - Direct method calls, no vtable lookup
- Compile-time optimization - When conditions are known statically, the compiler can eliminate the enum entirely
- Zero runtime cost - In the best case, compiles down to the concrete type with no abstraction overhead
When conditions are statically known at compile time, the sumtype version can be orders of magnitude faster because it compiles down to direct usage of the concrete type with zero abstraction overhead.
Additionally, sumtype can be used not only in functions but also in other contexts. For example, by using #[sumtype] in an expression block, you can initialize different types that implement the same trait based on certain conditions and assign them to a specific variable.
Here's an example to illustrate this:
# use sumtype::sumtype;
# let some_condition = true;
#[sumtype(sumtype::traits::Iterator)]
let mut iter = {
if some_condition {
sumtype!((0..5)) // Wraps the range iterator
} else {
sumtype!(vec![10, 20, 30].into_iter()) // Wraps the vector iterator
}
};
// Now `iter` can be used as a unified iterator type in the rest of the code
for value in iter {
println!("{}", value);
}
In this example, the #[sumtype] attribute is applied to an expression block, allowing different types that implement the same trait to be initialized based on the condition. These are then wrapped using the sumtype! macro and assigned to the iter variable, making it possible to work with them uniformly throughout the code. Unfortunately, this feature requires nightly Rust and #![feature(proc_macro_hygiene)], see Tracking issue for procedural macros and "hygiene 2.0".
Additionally, #[sumtype] can be used when defining traits as well as when implementing them. Here are examples for each case:
Using #[sumtype] with a trait definition:
# use sumtype::sumtype;
#[sumtype(sumtype::traits::Iterator)]
trait MyTrait {
fn get_iterator(&self, flag: bool) -> impl Iterator<Item = i32> {
if flag {
sumtype!((0..5)) // Wraps the range iterator
} else {
sumtype!(vec![10, 20, 30].into_iter()) // Wraps the vector iterator
}
}
}
In this example, #[sumtype] is applied to a trait definition. This could be useful for scenarios where you want to create sum types that can represent different implementations of a trait.
Using #[sumtype] with a trait implementation:
# use sumtype::sumtype;
#[sumtype(sumtype::traits::Iterator)]
trait MyTrait {
fn get_iterator(&self, flag: bool) -> impl Iterator<Item = i32> {
if flag {
sumtype!((0..5)) // Wraps the range iterator
} else {
sumtype!(vec![10, 20, 30].into_iter()) // Wraps the vector iterator
}
}
}
struct StructA;
#[sumtype(sumtype::traits::Iterator)]
impl MyTrait for StructA {
fn get_iterator(&self, _flag: bool) -> impl Iterator<Item = i32> {
sumtype!((0..5)) // Wraps a range iterator
}
}
Here, StructA implements MyTrait and uses sumtype! to wrap a range iterator.
Using #[sumtype] with a module definition:
# use sumtype::sumtype;
#[sumtype(sumtype::traits::Iterator)]
mod my_module {
pub struct MyStruct {
iter: sumtype!(),
}
impl MyStruct {
pub fn new(flag: bool) -> Self {
let iter = if flag {
sumtype!(0..5, std::ops::Range<u32>) // Wraps a range iterator
} else {
sumtype!(vec![10, 20, 30].into_iter(), std::vec::IntoIter<u32>) // Wraps a vector iterator
};
MyStruct { iter }
}
pub fn iterate(self) {
for value in self.iter {
println!("{}", value);
}
}
}
}
Supported Traits
The sumtype crate provides built-in support for several common traits:
sumtype::traits::Iterator- For types implementingstd::iter::Iteratorsumtype::traits::Read- For types implementingstd::io::Readsumtype::traits::Clone- For types implementingstd::clone::Clonesumtype::traits::Copy- For types implementingstd::marker::Copysumtype::traits::Debug- For types implementingstd::fmt::Debugsumtype::traits::Display- For types implementingstd::fmt::Displaysumtype::traits::Error- For types implementingstd::error::Error
More Examples
Working with different Read implementations
use sumtype;
Working with cloneable types
use sumtype;
Working with Debug and Display traits
use sumtype;
// Usage
let debug_item = get_debuggable;
println!; // "Debug: 42"
let display_item = get_displayable;
println!; // "Display: Static string"
Working with Error types
use sumtype;
use fmt;
// Define custom error types
;
;
// Usage
let error = get_error;
println!; // "Error: IO Error: Failed to read file"
println!; // Prints debug representation
// Can be used where std::error::Error is expected
handle_error;
Custom Traits
You can also define your own traits to work with sumtype using the #[sumtrait] attribute. This allows you to extend sumtype support to any trait that meets the sumtrait-safety requirements.
use ;
// Define a marker type (required for sumtrait)
;
// Define your custom trait
// Implement the trait for different types
;
;
// Use sumtype with your custom trait
See the #[sumtrait] documentation for more details on sumtrait-safety requirements and advanced usage patterns for creating custom sumtype-compatible traits.