#type #witness #type-id #equality #dispatch #no-alloc

no-std cisness

Runtime 'live witness' of two types being the same

3 stable releases

1.1.0 Jul 16, 2024
1.0.1 Jul 13, 2024

#811 in Rust patterns


Used in tisel

MPL-2.0 license

15KB
123 lines

cisness

Crate containing a type to "live-witness" that two types are the same ("cismutable").

In particular, it lets you assert that a codepath will not actually be executed at runtime if the types are NOT the same (and panic if it does), which is useful for - for instance - checking types via TypeId, and selecting operations with type-specific output where you want the type specificity can be transferred to the user.

The key structure you want to use this library, is LiveWitness. Using it "bare" from this library is somewhat clunky, but it can be used to build better abstractions (the author is working on a small crate called tisel that isn't published yet that will use this as a basis).

Example

Here's an example of how to use LiveWitness to invoke a trait method that has an associated type that can be output. Of course, in such a simple case, this isn't very useful, but it is much more useful in other situations.

use cisness::LiveWitness;

use std::{collections::HashMap, any::{TypeId, Any}};

pub trait PartyMember {
    type Age: core::fmt::Display;

    fn get_age(&self) -> Self::Age;
}

#[derive(Debug, Clone)]
pub struct Human {
    pub name: String,
    pub age: u32,
}


impl PartyMember for Human {
    type Age = u32;

    fn get_age(&self) -> Self::Age {
        self.age
    }
}


#[derive(Debug, Clone)]
pub struct Dragon {
    pub name: String
}

impl PartyMember for Dragon {
    type Age = &'static str;

    fn get_age(&self) -> Self::Age {
        "Unknowable and Eldritch"
    }
}

/// Party that stores the vocabulary "member" types in one hashmap dynamically.
/// Note that clearly, in this case, there are better ways to do it - but often that would not be the case. 
#[derive(Default)]
pub struct AdventuringParty {
    // Maps the `Member` type id to a vector of members of that type.
    members: HashMap<TypeId, Box<dyn Any>>
}


impl AdventuringParty {
    pub fn add_member<M: Any>(&mut self, member: M) {
        let new_member_typeid = TypeId::of::<M>();
        self.members
            .entry(new_member_typeid)
            .or_insert_with(|| Box::new(Vec::<M>::new()))
            .downcast_mut::<Vec::<M>>()
            .expect("<typeid> is mapped to Vec<type for typeid> and we just inited")
            .push(member);
    }

    // Note here - the better way to do this specific example would be to put a trait criteria on 
    // the `M` generic requiring `Ord`, and doing downcasting. However, for illustration purposes, we'll
    // be listing out supported types and manually implementing them, returning `None` for unorderable ages.
    pub fn get_oldest_age_of_type<M: Any + PartyMember>(&self) -> Option<<M as PartyMember>::Age> 
    {
        // Note that you could do this with `Any::downcast_ref`. However, that doesn't work if you can't 
        // get a value of `M`, and it doesn't work as well for more complex cases. 
        // To illustrate, we're going to use TypeId matching, even if for this case it would work better not to do that.
        let type_id = TypeId::of::<M>(); 
        match type_id {
            t if t == TypeId::of::<Human>() => {
                // Note here that we don't have to directly witness the type of the generic parameter, we can instead witness
                // the output (if we know it), or a vector. 
                // In this case, we're asserting that we'll only run through this code if the output type is the same as 
                // `Option<Human::Age>` - this lets us do "magical" coercions that will panic if we've made an error.
                let w = LiveWitness::<Option<<Human as PartyMember>::Age>, Option<M::Age>>::only_executed_if_same();
                let output = self.members.get(&t)
                    .and_then(|v| v.downcast_ref::<Vec<Human>>())
                    .map(|v| v.iter().map(PartyMember::get_age).max()).flatten();
                // Note here - our witness lets us turn this to the generic output, and will typecheck even if `M` != `Human` - because we don't go down
                // this path, it's fine.
                w.owned(output)
            },
            t if t == TypeId::of::<Dragon>() => {
                let w = LiveWitness::<Option<<Dragon as PartyMember>::Age>, Option<M::Age>>::only_executed_if_same();
                if !self.members.get(&t).is_some() { return None };
                // Note how at no point here did we actually have to downcast any values.
                let output = Some("[ELDRICH MEMETIC HAZARD]"); 
                w.owned(output)
            }
            _ => None
        } 
    }
}

The Name

Yes, it's intentional. The author is trans 🏳️‍⚧️, and finds it funny.

Dependencies

~18KB