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 {
&'static str as extract => extract.owned(v),
u8 | u16 | u32 | u64 | u128 => "unsigned-int",
i8 | i16 | i32 | i64 | i128 => "signed-int",
@_ => "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
},
@_ => 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![]);
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 {
typematch!((anyref src, out Target) {
(u32, &'static str | String as outwitness) => {
outwitness.owned("u32 not allowed".into())
},
| (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())
},
(&'static str as data, usize as ow) => {
ow.owned(data.len())
},
(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"))
},
(@_ 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")
})
}
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()
);
assert_eq!(build_transformed::<u32, &'static str>(&32u32), "u32 not allowed");
assert_eq!(build_transformed::<u64, String>(&10u64), "got an integer: 10".to_owned());
assert_eq!(build_transformed::<Vec<u8>, &'static str>(&vec![]), "unrecognised");
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}};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DidFail;
pub trait Message {
type Response;
fn handle_me(&self) -> Result<Self::Response, DidFail>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZeroStatus { Yes, No }
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())
}
}
#[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![]
}
}
}
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)
}
}
}
}
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) => {
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) => {
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)))
},
(@_ as message_typeid, @_) => {
let other_message_fallback =
self.other_registered_fallbacks.get(&message_typeid)?;
typematch!(
(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 {
common_u8: Fallbacks { primary_next_message: 1, fallback_messages: vec![2] },
common_u16: Fallbacks { primary_next_message: 3, fallback_messages: vec![] },
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)));
assert_eq!(my_registry.send_message(Some(&1u32)), Some(Ok(ZeroStatus::Yes)));
assert_eq!(my_registry.send_message(Some(&5i16)), Some(Ok(ZeroStatus::No)));
assert_eq!(my_registry.send_message::<String>(None), None);
my_registry.other_registered_fallbacks.insert(
TypeId::of::<String>(),
Box::new(Fallbacks::<String> {
primary_next_message: " hi people ".to_owned(),
fallback_messages: vec![]
})
);
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())
);