#dynamic-dispatch #dispatch #type-id #enums #dyn #specialization #no-alloc

no-std tisel

Effective type-based pseudodynamic dispatch to impls, enums and typeid

2 releases

0.1.1 Jul 20, 2024
0.1.0 Jul 20, 2024

#296 in Rust patterns

MPL-2.0 license

125KB
1.5K SLoC

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())
);

Dependencies