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 use
ing 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()]
.