Expand description
§tisel
- Type Impl Select
Tisel is a library designed to enable clean (and hopefully efficient) “type-parameter dynamic dispatch”, to enable effective use of vocabulary newtypes and similar constructs without unmanageable and unmaintainable lists of trait bounds, or impossible-to-express trait bounds when dealing with dynamic dispatch structures (such as handler registries and similar constructs, like those often used in Entity-Component Systems a-la Bevy).
The basic primitive is the typematch!
macro, which provides extremely powerful type-id matching capabilities (for both concrete Any
references and for type parameters without a value), and automatic “witnessing” of the runtime equivalence between types (for concrete references, this means giving you the downcasted reference, and for matching on type parameters, this means providing a LiveWitness
to enable conversions).
It can match on multiple types simultaneously (and types from different sources - for instance a generic parameter as well as the contents of an Any
reference), is capable of expressing complex |-combinations of types, and will check for properties like exhaustiveness by mandating a fallback arm (it will give you an error message if one is not present).
This primitive is useful for the creation of partially-dynamic registries, or registries where you want to delegate to monomorphic code in a polymorphic interface (which may be for many reasons including things like reducing trait bounds, dynamism, etc.), or multiple other potential uses.
A goal of this crate is also to make more ergonomic “k-implementation” enums for use in interfaces, that can be used to allow optional implementation while avoiding bounds-explosion - via some sort of trait dedicated to conversion to/from Any
-types or derivatives of them (like references), that can be implemented effectively. For now, we’ve focused on the macro as that is the most important core component.
§Basic Usage
Note that in almost all cases these would all be much better served by a trait. However, this crate is useful when dealing with registries where things are allowed only optionally have implementations, use with vocabulary newtypes associated with those registries, and similar things.
Single types can be matched on as follows:
use tisel::typematch;
use core::any::Any;
fn switcher<T: Any>(v: T) -> &'static str {
typematch!(T {
// note the use of a *witness* to convert the generic parameter type into the
// type it's been proven equal to by the match statement. The inverse can be done too,
// via .inverse() (which flips the witness), or via prefixing the matched type with
// `out`
&'static str as extract => extract.owned(v),
u8 | u16 | u32 | u64 | u128 => "unsigned-int",
i8 | i16 | i32 | i64 | i128 => "signed-int",
// Note that we use `@_` instead of `_`
// This is due to limitations of macro_rules!. But it means the same as `_`
@_ => "unrecognised"
})
}
assert_eq!(switcher("hello"), "hello");
assert_eq!(switcher(4u32), "unsigned-int");
assert_eq!(switcher(-3), "signed-int");
assert_eq!(switcher(vec![89]), "unrecognised");
You can also match on Any
references:
use tisel::typematch;
use core::any::Any;
fn wipe_some_stuff(v: &mut dyn Any) -> bool {
typematch!(anymut (v = v) {
String as value => {
*value = String::new();
true
},
&'static str as value => {
*value = "";
true
},
Vec<u8> as value => {
*value = vec![];
true
},
// fallback action - does nothing.
@_ => false
})
}
let mut string = String::new();
let mut static_string = "hiii";
let mut binary_data: Vec<u8> = vec![8, 94, 255];
let mut something_else: Vec<u32> = vec![390, 3124901, 901];
let data: [&mut dyn Any; 4] = [&mut string, &mut static_string, &mut binary_data, &mut something_else];
assert_eq!(data.map(wipe_some_stuff), [true, true, true, false]);
assert_eq!(string, "".to_owned());
assert_eq!(static_string, "");
assert_eq!(binary_data, vec![]);
// Because there was no implementation for Vec<u32>, nothing happened
assert_eq!(something_else, vec![390, 3124901, 901]);
It’s possible to match on multiple sources of type information simultaneously - it works just like a normal match statement on a tuple of values:
use tisel::typematch;
use core::any::{Any, type_name};
fn build_transformed<Source: Any, Target: Any>(src: &Source) -> Target {
// In this case, we could witness that `source` was the same as each of the LHS types
// using a live witness, and cismute it from the argument to the specific type.
//
// This would be more efficient. However, to demonstrate using multiple distinct
// sources of type information simultaneously (and the anyref/anymut syntax in a multi-
// typematch), we'll convert `v` into an `Any` reference.
//
// Also remember that it's very easy to accidentally use a container of a `dyn Any`
// as a `dyn Any` itself, when you want to use the inner type. See the notes in
// `core::any`'s documentation to deal with this.
typematch!((anyref src, out Target) {
// This one will be checked first, and override the output
(u32, &'static str | String as outwitness) => {
outwitness.owned("u32 not allowed".into())
},
// We can use |-patterns, both in a single pattern and between patterns for
// a single arm.
// In this case, we could merge the two patterns into one, but we won't
| (u8 | u16 | u32 | u64 | usize | u128 as data, String as outwitness)
| (i8 | i16 | i32 | i64 | usize | i128 as data, String as outwitness) => {
outwitness.owned(format!("got an integer: {data}"))
},
| (usize | isize, &'static str | String as ow) => {
ow.owned("size".into())
},
// Get the lengths of static strings.
(&'static str as data, usize as ow) => {
ow.owned(data.len())
},
// This will never be invoked if the input is a 'static str and output is a usize
// It also demonstrates the ability to create local type aliases to the matched
// types. Note - this will not work if you try and match on a generic parameter
// because rust does not allow type aliases to generic parameters inside inner
// scopes (though you can just use the generic parameter directly in this case).
(type In = String | &'static str, usize | u8 | u16 | u32 | u64 | u128 as ow) => {
ow.owned(type_name::<In>().len().try_into().expect("should be short"))
},
// Witnessing an unconstrained type input will still get you useful things.
// For instance, when dealing with `Any` references, it will give you the
// raw `Any` reference directly
(@_ as raw_data, String as ow) => {
let typeid = raw_data.type_id();
ow.owned(format!("type_id: {typeid:?}"))
},
(@_, &'static str | String as ow) => ow.owned("unrecognised".into()),
(@_, @_) => panic!("unrecognised")
})
}
// Length extraction
// Types are explicit to make it clear what's happening.
// Even though this is a string - int combo, it's extracting the actual length instead
// of the typename length, because of the matcher earlier in the list.
assert_eq!(build_transformed::<&'static str, usize>(&"hiii"), 4usize);
assert_eq!(build_transformed::<String, u8>(
&"hello world".to_owned()),
type_name::<String>().len().try_into().unwrap()
);
// See the disallowed u32
assert_eq!(build_transformed::<u32, &'static str>(&32u32), "u32 not allowed");
// formatted input
assert_eq!(build_transformed::<u64, String>(&10u64), "got an integer: 10".to_owned());
// Unrecognised input
assert_eq!(build_transformed::<Vec<u8>, &'static str>(&vec![]), "unrecognised");
// This would panic, as it would hit that last catch-all branch
// assert_eq!(build_transformed::<Vec<u8>, u32>(&vec![]), 0);
§Examples
Here are some examples to get you started. Many of these could be done in better ways using other methods, but they are here to illustrate basic usage of this library.
§Basic Example (Typematch Macro) - Common Fallback
This example illustrates the powerful capability of typematch
for composing Any
, with bare type matching, and similar, to create registerable fallback handlers while also storing ones that you know will be available at compile time, statically.
use tisel::typematch;
use std::{collections::HashMap, any::{Any, TypeId}};
/// basic error type
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DidFail;
/// Trait for some message handleable by your handler.
pub trait Message {
type Response;
/// Handle the message
fn handle_me(&self) -> Result<Self::Response, DidFail>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZeroStatus { Yes, No }
// Implement the message trait for ints, but artificially induce fallibility by making it
// not work when the value is 1 or 3
macro_rules! ints {
($($int:ty)*) => {
$(impl Message for $int {
type Response = ZeroStatus;
fn handle_me(&self) -> Result<Self::Response, DidFail> {
match self {
1 | 3 => Err(DidFail),
0 => Ok(ZeroStatus::Yes),
_ => Ok(ZeroStatus::No)
}
}
})*
}
}
ints!{u8 u16 u32 u64 i8 i16 i32 i64};
impl Message for String {
type Response = String;
fn handle_me(&self) -> Result<Self::Response, DidFail> {
Ok(self.trim().to_owned())
}
}
/// Basically an example of things we can store
#[derive(Debug, Clone)]
pub struct Fallbacks<T> {
pub primary_next_message: T,
pub fallback_messages: Vec<T>
}
impl <T> Fallbacks<T> {
pub fn iter(&self) -> impl Iterator<Item = &'_ T> {
core::iter::once(&self.primary_next_message).chain(self.fallback_messages.iter())
}
}
impl<T> From<T> for Fallbacks<T> {
fn from(v: T) -> Self {
Self {
primary_next_message: v,
fallback_messages: vec![]
}
}
}
/// Message fallback registry, with inline stored values and fallbacks, plus the ability to
/// register new message types and fallbacks. This illustrates how to do fallbacks and similar
/// such things as well
pub struct MyRegistry {
pub common_u8: Fallbacks<u8>,
pub common_u16: Fallbacks<u16>,
pub common_u32: Fallbacks<u32>,
pub other_registered_fallbacks: HashMap<TypeId, Box<dyn Any>>,
}
impl MyRegistry {
fn send_message_inner<T: Message>(
message: Option<&T>,
fallbacks: &Fallbacks<T>
) -> Result<T::Response, DidFail> {
let try_order = message.into_iter().chain(fallbacks.iter());
let mut tried = try_order.map(Message::handle_me);
let mut curr_result = tried.next().unwrap();
loop {
match curr_result {
Ok(v) => break Ok(v),
Err(e) => match tried.next() {
Some(new_res) => { curr_result = new_res; },
None => break Err(e)
}
}
}
}
/// "send" one of the example "messages". If you provide one, then it uses the value you
/// provided and falls back. If you do not provide one, then it uses the primary fallback
/// immediately.
///
/// This also automatically merges signed and unsigned small ints (u8/i8, u16/i16, and
/// u32/i32), disallowing negative values.
pub fn send_message<T: Message<Response: Any> + Any>(
&self,
custom_message: Option<&T>
) -> Option<Result<T::Response, DidFail>> {
typematch!((T, out T::Response) {
(u8 | i8 as input_witness, ZeroStatus as zw) => {
let fallback = &self.common_u8;
let message: Option<u8> = custom_message
.map(|v| input_witness.reference(v))
.cloned()
.map(TryInto::try_into)
.map(|v| v.expect("negative i8 not allowed!"));
Some(Self::send_message_inner(
message.as_ref(),
&self.common_u8
).map(|r| zw.owned(r)))
},
(u16 | i16 as input_witness, ZeroStatus as zw) => {
// similar
let fallback = &self.common_u16;
let message: Option<u16> = custom_message.map(|v| input_witness.reference(v)).cloned().map(TryInto::try_into).map(|v| v.expect("negative i16 not allowed!"));
Some(Self::send_message_inner(
message.as_ref(),
&self.common_u16
).map(|r| zw.owned(r)))
},
(u32 | i32 as input_witness, ZeroStatus as zw) => {
// similar
let fallback = &self.common_u32;
let message: Option<u32> = custom_message.map(|v| input_witness.reference(v)).cloned().map(TryInto::try_into).map(|v| v.expect("negative i32 not allowed!"));
Some(Self::send_message_inner(
message.as_ref(),
&self.common_u32
).map(|r| zw.owned(r)))
},
// Using type aliases without explicit constraints is possible, but only when
// you're matching directly against a non-generic type. We can't use one here,
// because we're matching against a type derived from a generic parameter, and
// Rust will not let you create type aliases in sub-blocks that reference
// generic parameters (you can just use the generic parameter directly,
// though).
//
// We can also now use this to fall-back to retrieving from the map. Not only
// this, but we can use the type alias to then easily extract types directly
// from the map.
(/*type OtherMessage = */@_ as message_typeid, @_) => {
let other_message_fallback =
self.other_registered_fallbacks.get(&message_typeid)?;
typematch!(
// Here's an example of using an anyref. These can be used in full
// combination with matching on types or on anymut.
//
// Important to note here is that, for Box, we need to make sure to be
// getting an &dyn Any, not Box<dyn Any>, because the latter itself
// implements Any
(anyref (fallbacks = other_message_fallback.as_ref())) {
(Fallbacks<T> as fallbacks) => {
Some(Self::send_message_inner(custom_message, fallbacks))
},
(@_) => unreachable!(
"wrong type for registered fallbacks"
)
}
)
}
})
}
}
let mut my_registry = MyRegistry {
// This one will fall back to a working `2`
common_u8: Fallbacks { primary_next_message: 1, fallback_messages: vec![2] },
// This one will simply fail unless another message is asked to be sent
common_u16: Fallbacks { primary_next_message: 3, fallback_messages: vec![] },
// This one will succeed :)
common_u32: Fallbacks { primary_next_message: 0, fallback_messages: vec![] },
other_registered_fallbacks: HashMap::new()
};
assert_eq!(my_registry.send_message(Some(&4u32)), Some(Ok(ZeroStatus::No)));
// this is a failing one so should fall back to zero because it's u32
assert_eq!(my_registry.send_message(Some(&1u32)), Some(Ok(ZeroStatus::Yes)));
// this illustrates the way we forced the signed ones to use the unsigned impls
assert_eq!(my_registry.send_message(Some(&5i16)), Some(Ok(ZeroStatus::No)));
// Illustrates the non-registered fallback
assert_eq!(my_registry.send_message::<String>(None), None);
// Registering it. This would in reality be abstracted behind some sort of method
my_registry.other_registered_fallbacks.insert(
TypeId::of::<String>(),
Box::new(Fallbacks::<String> {
primary_next_message: " hi people ".to_owned(),
fallback_messages: vec![]
})
);
// Now it can pull in the impl
assert_eq!(
my_registry.send_message::<String>(None).unwrap(),
Ok("hi people".to_owned())
);
assert_eq!(
my_registry.send_message(Some(&"ferris is cool ".to_owned())).unwrap(),
Ok("ferris is cool".to_owned())
);
Macros§
- This is a tt-munching parser that checks an arm for deletion to jump to [__typematch_arm_deleted] if that is the case.
- Takes the generated and classified parts described by [__typematch_arm_check_delete], and actually creates the components. This provides certain very useful capabilities - for example, it does shorthand match stuff for fully-unconstrained arms, and is capable of creating the
pattern
,pattern_guard
, andmeta
components directly, as the format produced by the previous macro is amenable to efficient and uniform construction of those. - Generate a deleted arm
- Generate a nice compile_error directive for a typematch arm, but in just a single place.
- Entry point for generating each
crate::typematch
match arm. - Macro that makes the type alias type directly if possible.
- Macro that actually makes the witness value directly if possible.
- This is the “submacro” which actually generates the match statement and surrounding information
- This is a slightly-but-not-mostly tt-munching parser that constructs information about the TypeId generator expressions. It can’t avoid the need to munch because of parsing ambiguities.
- Macro to simplify and modularise the parsing of “single-mode” match and separate out all the commonalities, to dispatch into [__typematch_parse_typeid_generators].
- This directly associates typeid generator information per-index with the various match arm infos per-index into one iterable chunk
- Subcomponent of [__typematch_top_level_arms_branch_extract] that executes commands to add to the (now splayed out) match arm prefix data. This used to be part of one big macro, so most of the documentation is in [__typematch_top_level_arms_branch_extract].
- Intermediary macro that takes the output from [__typematch_top_level_arms_syntax_split] and puts it into two places:
- This is a recursive part of
crate::typematch
that takes the arms plus the next type-branches for each arm, and splays out an arm for each possible |-type that extends the original arm in the process of branching. - Subcomponent of [
__typematch_top_level_arms_branch_extract
], which has much more documentation (they used to be one giant macro). - Verify that there is at least one exhaustive pattern in all the parsed patterns.
- Actual implementation of [__typematch_top_level_arms_branch_verify_exhaustive] using a much-simplified list form.
- Something very important to know is that we can in fact “replicate” repeated data inside nested repeats, as long as there is no ambiguous iteration. The most important thing is to avoid constructing any
match
statements before we can unambiguously iterate over all concrete match arms, as you can’t build match arms as macro output - given that we have such a complex association between match arms and the corresponding expression, this makes things harder. - Switch over
k
types (or type-id-expressions), and get witness values. Also works for single ones.