Expand description
Rust has a built in tuple (A, B, C, ...)
to represent a “product” of types. The language lacks a generic syntax for the converse:
a choice among multiple types, also known as a sum type (or “coproduct”) A + B + C + ...
.
This library provides a pattern and macro to bridge this gap.
§Example
// We can instantiate a "heterogenous" `Vec` without a custom `enum`.
use choice::choice;
struct A;
struct B;
struct C;
let choices: Vec<choice![A, B, C]> = vec![
choice!(0 <- A),
choice!(1 <- B),
choice!(2 <- C),
];
// Furthermore, by implementing a trait for two `Choice` forms...
use choice::{Choice, Never};
trait T {}
impl<T1: T> T for Choice<T1, Never> {}
impl<T1: T, T2: T> T for Choice<T1, T2> {}
// ... then for types that implement the trait, any `Choice` between those types also
// implements the trait.
impl T for A {}
impl T for B {}
impl T for C {}
fn f(t: impl T) {}
for x in choices {
f(x); // accepts values of type `choice![A, B, C]`
}
§Composition Pattern
The underlying pattern may be a bit counterintuitive the first time you see it. The first step
is to use Choice::new
to build a base variant on top of Never
:
use choice::{Choice, Never};
let no_real_choice: Choice<u64, Never> = Choice::new(42);
The Never
type is uninhabitable and only serves to seed the pattern, so effectively we have a
“choice” between N=1 types in the example above because an instance of the type can only hold a
u64
. Calling Choice::or
extends a type to offer one more choice, inductively enabling a
choice between N+1 types.
let two_types_choice1: Choice<&'static str, Choice<u64, Never>> =
Choice::new(42).or();
You can build an instance of the same Choice
type that holds the other inner type by simply
calling Choice::new
:
let two_types_choice2: Choice<&'static str, Choice<u64, Never>> =
Choice::new("Forty two");
The above two examples share a type, so you can embed them in a collection:
let u64_or_string_vec: Vec<Choice<&'static str, Choice<u64, Never>>> = vec![
Choice::new(42).or(),
Choice::new("Forty two"),
Choice::new(24).or(),
Choice::new("Twenty four"),
];
This pattern composes to allow additional choices:
let many: Vec<Choice<&'static str, Choice<i8, Choice<char, Never>>>> = vec![
Choice::new("-INFINITY"),
Choice::new(-1 ).or(),
Choice::new('0' ).or().or(),
Choice::new(1 ).or(),
Choice::new("INFINITY" ),
];
§Trait Composition
Custom enum
s serve a similar role but generally lack support for the kind of composition that
Choice
provides. For example, if types A
and B
implement trait T
, a custom enum AOrB
could also implement that trait. Unfortunately any differing choice between types would need to
reimplement this trait, e.g. necessitating a type AOrCOrD
for another scenario that needs to
choose between types A
, C
, and D
.
By implementing trait T
for Choice<A: T, Never>
and Choice<A: T, B: T>
, the trait is also
implemented for any combination of choices. See the Example section above or
alternatively
stateright::actor::Actor
for a real-world example from another library.
§Macro
The choice!
macro provides syntactic sugar for a type or value of the above pattern, which
is particularly useful when there are many choices:
let x1: choice![u64, &'static str, char, String, i8] =
choice!(2 <- 'x');
let x2: Choice<u64, Choice<&'static str, Choice<char, Choice<String, Choice<i8, Never>>>>> =
Choice::new('x').or().or();
assert_eq!(x1, x2);
That macro also provides syntactic sugar for pattern matching on a Choice
. Rust is unable to
determine that the base case Never
in uninhabited, so there is also a form to appease the
exhaustiveness checker.
let c: choice![u8, char] = choice!(1 <- '?');
match c {
choice!(0 -> v) => {
panic!("Unexpected match: {}", v);
}
choice!(1 -> v) => {
assert_eq!(v, '?');
}
choice!(2 -> !) => {
unreachable!();
}
}
§Features
Enable the serde
feature for serialization/deserialization.
Re-exports§
Macros§
- choice
- Syntactic sugar for (1) a
Choice
among types, (2) a value inhabiting aChoice
, and (3) destructuring for one of these values.