dtype_dispatch solves the problem of interop between generic and
dynamically typed (enum) containers.
This is a common problem in numerical libraries (think numpy, torch, polars): you have a variety of data types and data structures to hold them, but every function involves matching an enum or converting from a generic to an enum.
Example with i32 and f32 data types for dynamically-typed vectors,
supporting .length() and .add(other) operations, plus generic
new and downcast functions:
// register our two macros, `define_an_enum` and `match_an_enum`, constrained
// to the `Dtype` trait, with our variant => type mapping:
build_dtype_macros!;
// define any enum holding a Vec of any data type!
define_an_enum!;
// we could also use `DynArray::I32()` here, but just to show we can convert generics:
let x_dynamic = new.unwrap;
let x_doubled_generic = x_dynamic.add..unwrap;
assert_eq!;
Compare this with the same API written manually:
use ;
let x_dynamic = new;
let x_doubled_generic = x_dynamic.add.;
assert_eq!;
That's a lot of match/if clauses and repeated boilerplate!
It would become impossible to manage if we had 10 data types and multiple
containers (e.g. sparse arrays).
dtype_dispatch elegantly solves this with a single macro that generates two
powerful macros for you to use.
These building blocks can solve almost any dynamic<->generic data type dispatch
problem:
Comparisons
Box<dyn> |
enum_dispatch |
dtype_dispatch |
|
|---|---|---|---|
| convert generic -> dynamic | ✅ | ❌* | ✅ |
| convert dynamic -> generic | ❌ | ❌* | ✅ |
| call trait fns directly | ⚠️** | ✅ | ❌ |
| match with type information | ❌️ | ❌ | ✅ |
| stack allocated | ❌️ | ✅ | ✅ |
| variant type requirements | trait impl | trait impl | container<trait impl> |
*Although enum_dispatch supports From and TryInto, it only works for
concrete types (not in generic contexts).
**Trait objects can only dispatch to functions that can be put in a vtable,
which is annoyingly restrictive.
For instance, traits with generic associated functions can't be put in a
Box<dyn>.
All enums are #[non_exhaustive] by default, but the matching macros generated
handle wildcard cases and can be used safely in downstream crates.
Limitations
At present, enum and container type names must always be a single identifier.
For instance, Vec will work, but std::vec::Vec and Vec<Foo> will not.
You can satisfy this by useing your type or making a type alias of it,
e.g. type MyContainer<T: MyConstraint> = Vec<Foo<T>>.
It is also mandatory that you place exactly one attribute when defining each
enum, e.g. with a #[derive(Clone, Debug)].
If you don't want any attributes, you can just do #[derive()].