2 个版本
0.1.1 | 2024年7月20日 |
---|---|
0.1.0 | 2024年7月20日 |
252 在 Rust 模式
213 每月下载量
125KB
1.5K SLoC
tisel
- Type Impl Select
蒂塞尔是一个库,旨在实现干净(并希望有效)的“类型参数动态分发”,以便在没有无法管理的 trait 约束列表或处理动态分发结构(如处理程序注册表以及类似的结构,例如在 Entity-Component 系统中常用)时无法表达 trait 约束的情况下,有效地使用新类型词汇和类似结构。
其基本原语是 typematch!
宏,它提供了非常强大的类型 id 匹配能力(对于具体 Any
引用和没有值的类型参数),以及自动“见证”类型之间的运行时等价性(对于具体引用,这意味着提供下转换引用,对于类型参数匹配,这意味着提供一个 LiveWitness
以启用转换)。
它可以同时匹配多个类型(并且来自不同来源的类型 - 例如泛型参数以及 Any
引用的内容),能够表达复杂的 |-类型组合,并会通过强制一个后备分支来检查属性如详尽性(如果不存在后备分支,它将提供错误消息)。
这个原语对于创建部分动态注册表或想要在多态接口中委派到单态代码的注册表(这可能有多种原因,包括减少 trait 约束、动态性等)以及其他多种潜在用途非常有用。
此包的另一个目标是创建更符合人体工程学的“k实现”枚举,用于接口,可以实现可选实现的同时避免边界爆炸——通过某种专门的特质,用于在Any
类型或其派生类型(如引用)之间进行转换,从而实现有效的转换。目前,我们主要集中在宏上,因为这是最重要的核心组件。
基本用法
请注意,在几乎所有情况下,这些都可以通过特质更好地实现。然而,当处理允许可选实现的事物注册表时,此包非常有用,使用与这些注册表相关的新类型词汇,以及类似的事物。
可以对单种类型进行匹配,如下所示
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");
您还可以匹配Any
引用
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]);
可以同时匹配多个类型信息来源——它的工作方式与在值元组的正常匹配语句上匹配相同
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);
示例
以下是一些示例,以帮助您开始。其中许多可以通过其他方法以更好的方式完成,但它们在这里是为了说明此库的基本用法。
基本示例(Typematch 宏)- 常见回退
此示例展示了typematch
的强大功能,它可以组合Any
,裸类型匹配以及类似功能,以创建可注册的回退处理器,同时静态地存储在编译时已知的处理器。
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())
);