Skip to main content

Crate sumtype

Crate sumtype 

Source
Expand description

§sumtype crate Latest Version Documentation GitHub Actions

In Rust, returning one of several different types from a function depending on its arguments is awkward: even when the types share the same interface, they remain distinct concrete types, so you cannot return them directly from a single function. The usual workaround is Box<dyn Trait>, but it has drawbacks — it allocates on the heap, it is not a zero-cost abstraction, and the boxed type often has to be 'static.

For example:

// Example with Iterator trait
fn conditional_iterator(flag: bool) -> Box<dyn Iterator<Item = i32>> {
    if flag {
        Box::new(0..10) as Box<dyn Iterator<Item = i32>>
    } else {
        Box::new(vec![1, 2, 3].into_iter()) as Box<dyn Iterator<Item = i32>>
    }
}

// Example with Read trait  
fn conditional_reader(use_file: bool) -> Box<dyn std::io::Read> {
    if use_file {
        Box::new(std::fs::File::open("data.txt").unwrap()) as Box<dyn std::io::Read>
    } else {
        Box::new(std::io::Cursor::new(b"hello world")) as Box<dyn std::io::Read>
    }
}

Each function returns one of two different concrete types that implement the same trait (Iterator or Read). Box<dyn Trait> lets them share a return type, but only at the cost of the drawbacks above.

This crate solves the problem by generating a single anonymous sum type for each context annotated with the #[sumtype] attribute. The sumtype!(expr) macro wraps an expression in that sum type, so the otherwise-distinct types all become one type within the same #[sumtype] context — which is what lets a single function return any of them. The sum type is a plain enum, so it needs no heap allocation, and it remains a zero-cost abstraction: when the branch is known at compile time, the compiler can collapse the enum away and leave only the concrete type.

The same functions written with sumtype:

use sumtype::sumtype;

// Iterator example
#[sumtype(sumtype::traits::Iterator)]
fn conditional_iterator(flag: bool) -> impl Iterator<Item = i32> {
    if flag {
        sumtype!(0..10) // Wraps the range iterator
    } else {
        sumtype!(vec![1, 2, 3].into_iter()) // Wraps the vector iterator
    }
}

// Read example
#[sumtype(sumtype::traits::Read)]
fn conditional_reader(use_cursor: bool) -> impl std::io::Read {
    if use_cursor {
        sumtype!(std::io::Cursor::new(b"hello world"))
    } else {
        sumtype!(std::io::Cursor::new(vec![1, 2, 3, 4, 5]))
    }
}

Here #[sumtype] generates a sum type that can hold any of the concrete types implementing the specified trait, and sumtype! wraps each expression so they share that type within the function. Because the sum type is a plain enum, it avoids heap allocation, and when the branch is known at compile time the compiler optimizes the abstraction away.

§Zero-Cost Abstraction vs Box<dyn Trait>

A key advantage of sumtype over Box<dyn Trait> is its zero-cost abstraction.

Benefits of sumtype:

  1. No heap allocation - Uses stack-allocated enum variants
  2. Static dispatch - Direct method calls, no vtable lookup
  3. Compile-time optimization - When conditions are known statically, the compiler can eliminate the enum entirely
  4. Zero runtime cost - In the best case, compiles down to the concrete type with no abstraction overhead

When the branch is known at compile time, the sumtype version can be far faster than a boxed trait object, because it compiles down to direct use of the concrete type with no abstraction overhead.

§Difference from std::any::Any

std::any::Any can also hold a value that is one of many concrete types, but it solves a different problem. Any is about type erasure and recovery: you store a value as Box<dyn Any> (or &dyn Any) and later try to recover its original type with downcast_ref::<T>(). sumtype is about unifying a fixed set of types behind a shared trait, so that you keep calling that trait’s methods directly.

sumtypedyn Any
Set of typesclosed — known per #[sumtype] contextopen — any 'static type
Calling the shared traitdirect; the sum type implements itnot possible — you must downcast to a concrete type first
Recovering the concrete typesecondary — via Downcast when 'staticdowncast, which can fail at runtime
Allocationnone (stack-allocated enum)usually Box (heap)
Dispatchstatic, and often optimized away entirelyruntime TypeId comparison
'static requirementonly for Downcast; not for the core typerequired (Any: 'static)

In short, reach for dyn Any when you genuinely need runtime reflection — to stash values of an unknown type and recover them later. Reach for sumtype when the set of types is known and you simply want them to share a trait, without the cost (or the 'static requirement) of Box<dyn Trait> or dyn Any.

§Recovering a concrete type

When you do need to recover the original type — dyn Any’s use case — sumtype offers the Downcast trait, implemented automatically for every generated sum type. Name the target type(s) in the return bound (+ Downcast<To>), and it works with the ordinary type-less sumtype!(expr) form:

use sumtype::{sumtype, Downcast};

#[sumtype(sumtype::traits::Debug)]
fn make(b: bool) -> impl std::fmt::Debug + Downcast<u32> + Downcast<String> {
    if b { sumtype!(1u32) } else { sumtype!(String::from("hi")) }
}

let v = make(true);
assert_eq!(Downcast::<u32>::downcast_ref(&v), Some(&1u32)); // the active variant
assert_eq!(Downcast::<String>::downcast_ref(&v), None);     // wrong type

// Move the value out, or get the sum type back unchanged on a miss:
assert_eq!(Downcast::<u32>::downcast(make(true)).ok(), Some(1u32));

Because downcasting checks the wrapped type at runtime (via TypeId), both the target and the wrapped types must be 'static (so it is unavailable for sum types that wrap a borrowed value). downcast_ref/downcast_mut are allocation-free; the owning downcast boxes the value briefly to move it out. To is a trait parameter, so it is chosen by a type annotation or a Downcast::<To>::… call (not a method turbofish), which also lets Downcast<To> be used as a generic bound: fn f<E: Downcast<u32>>(e: E) { … }. (Exposing the named sum type — a -> sumtype!() return or a field — also works and makes every target type available at once.)

§Using #[sumtype] in other contexts

#[sumtype] is not limited to functions. Applied to an expression block, for instance, it lets you initialize one of several trait-implementing types based on a condition and bind the result to a variable:

#[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);
}

Here #[sumtype] is applied to an expression block: each branch is wrapped with sumtype! and bound to iter, which can then be used uniformly in the rest of the code. Note that this form requires nightly Rust and #![feature(proc_macro_hygiene)] — see the tracking issue for procedural macros and “hygiene 2.0”.

#[sumtype] can also be applied when defining a trait and when implementing one. Here is an example of each.

Using #[sumtype] with a trait definition:

#[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
        }
    }
}

Here #[sumtype] is applied to a trait definition — useful when a trait’s default method body needs to return a sum type.

Using #[sumtype] on a trait and on an implementation of it for a concrete type:

#[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, wrapping a range iterator with sumtype!.

Using #[sumtype] with a module definition:

#[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:

§More Examples

§Working with different Read implementations
use sumtype::sumtype;

#[sumtype(sumtype::traits::Read)]
fn get_reader(source: &str) -> impl std::io::Read {
    match source {
        "memory" => sumtype!(std::io::Cursor::new(b"Hello from memory")),
        "empty" => sumtype!(std::io::empty()),
        _ => sumtype!(std::io::Cursor::new(vec![0u8; 1024])),
    }
}
§Working with cloneable types
use sumtype::sumtype;

#[sumtype(sumtype::traits::Clone)]
fn get_cloneable(use_string: bool) -> impl Clone {
    if use_string {
        sumtype!(String::from("Hello"))
    } else {
        sumtype!(vec![1, 2, 3])
    }
}
§Working with Debug and Display traits
use sumtype::sumtype;

#[sumtype(sumtype::traits::Debug)]
fn get_debuggable(use_int: bool) -> impl std::fmt::Debug {
    if use_int {
        sumtype!(42i32)
    } else {
        sumtype!(String::from("hello"))
    }
}

#[sumtype(sumtype::traits::Display)]
fn get_displayable(use_int: bool) -> impl std::fmt::Display {
    if use_int {
        sumtype!(42i32)
    } else {
        sumtype!("Static string")
    }
}

// Usage
let debug_item = get_debuggable(true);
println!("Debug: {:?}", debug_item); // "Debug: 42"

let display_item = get_displayable(false);
println!("Display: {}", display_item); // "Display: Static string"
§Working with Error types
use sumtype::sumtype;
use std::fmt;

// Define custom error types
#[derive(Debug)]
struct IoError(String);

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "IO Error: {}", self.0)
    }
}

impl std::error::Error for IoError {}

#[derive(Debug)]
struct ParseError(String);

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Parse Error: {}", self.0)
    }
}

impl std::error::Error for ParseError {}

#[sumtype(sumtype::traits::Error)]
fn get_error(is_io_error: bool) -> impl std::error::Error {
    if is_io_error {
        sumtype!(IoError("Failed to read file".to_string()))
    } else {
        sumtype!(ParseError("Invalid format".to_string()))
    }
}

// Usage
let error = get_error(true);
println!("Error: {}", error); // "Error: IO Error: Failed to read file"
println!("Debug: {:?}", error); // Prints debug representation

// Can be used where std::error::Error is expected
fn handle_error(e: impl std::error::Error) {
    println!("Handling error: {}", e);
}

handle_error(get_error(false));

§Custom Traits

You can make your own traits work with sumtype using the #[sumtrait] attribute, which extends support to any trait that satisfies the sumtrait-safety requirements.

use sumtype::{sumtype, sumtrait};

// Define a marker type (required for sumtrait)
pub struct MyTraitMarker(std::convert::Infallible);

// Define your custom trait
#[sumtrait(marker = MyTraitMarker)]
pub trait MyCustomTrait {
    fn process(&self) -> String;
}

// Implement the trait for different types
struct TypeA;
impl MyCustomTrait for TypeA {
    fn process(&self) -> String {
        "Processing with TypeA".to_string()
    }
}

struct TypeB;
impl MyCustomTrait for TypeB {
    fn process(&self) -> String {
        "Processing with TypeB".to_string()
    }
}

// Use sumtype with your custom trait
#[sumtype(MyCustomTrait)]
fn get_processor(use_a: bool) -> impl MyCustomTrait {
    if use_a {
        sumtype!(TypeA)
    } else {
        sumtype!(TypeB)
    }
}

See the #[sumtrait] documentation for the full sumtrait-safety requirements and more advanced patterns for building custom sumtype-compatible traits.

Modules§

traits
Mock traits targeted by the #[sumtype] macro. They make the corresponding std/core traits usable with #[sumtype].

Traits§

Downcast
Recovers a concrete type To from a #[sumtype] sum type.

Attribute Macros§

sumtrait
Makes a user-defined trait usable with #[sumtype].
sumtype
Enables the sumtype!(..) macro within the annotated context.