Skip to content

ACP: Add const fn TypeId::matches for comparing type ids in consts #231

Closed
@KodrAus

Description

@KodrAus

Proposal

Problem statement

To provide an API for comparing TypeIds in const contexts that can be stabilized "soon" along with TypeId::of without depending on const in trait impls. This API should make stabilization of TypeId in const contexts uncontentious, but isn't interpreted as a commitment to actually do that stabilization.

Motivating examples or use cases

TypeIds can be used in const contexts as a limited form of specialization; they can be used to dispatch at compile-time based on the type of a value. As a real-world example, I have a library that allows capturing values using a standard trait, like fmt::Display, but specializes internally when that value is a primitive like a string or integer:

use std::{any::TypeId, fmt};

#[derive(Debug)]
pub enum Value<'a> {
    I32(i32),
    // Other variants for other primitives...
    Debug(&'a dyn fmt::Debug)
}

impl<'a> Value<'a> {
    pub fn capture_debug<T: fmt::Debug + 'static>(value: &'a T) -> Value<'a> {
        value_from_primitive(value).unwrap_or(Value::Debug(value))
    }
}

// NOTE: Can't currently be `const`; support was removed in https://github.com/rust-lang/rust/pull/103291
// `T` is unsized so `str` can be supported as well
const fn value_from_primitive<'a, T: ?Sized + 'static>(value: &'a T) -> Option<Value<'a>> {
    let id = TypeId::of::<T>();
    
    if id == TypeId::of::<i32>() {
        // Using `TypeId` instead of `Any::downcast` because `T` is unsized; so we can't convert into a `dyn Any`
        // without losing the `'static` bound
        // SAFETY: `T` has been asserted to be `i32`
        return Some(Value::I32(unsafe { *(value as *const T as *const i32) }))
    }

    // possibly many other branches...
    
    None
}

fn main() {
    // Prints `I32(42)`
    println!("{:?}", Value::capture_debug(&42i32));
    
    // Prints `Debug(42)`
    println!("{:?}", Value::capture_debug(&42u32));
}

This pattern is useful when working with dynamic data such as when templating or wiring up loosely-coupled state. Unfortunately, it doesn't currently work even on nightly because structural-matching of TypeIds has been removed (for perfectly valid reasons). The only thing you can do with a TypeId is compare it with other TypeIds, so without some const way to compare them, TypeId is currently useless in const contexts.

Solution sketch

This proposes TypeId::matches; an API for asserting two TypeIds match at compile-time that could be stabilized "soon" alongside TypeId::of to make TypeIds usable in const functions:

impl TypeId {
    /// Whether this type id is the same as `other`.
    ///
    /// If a `TypeId` matches another it means they were both instantiated from
    /// the same generic type `T`.
    ///
    /// This method is equivalent to equality, but can be used at compile-time.
    ///
    /// # Examples
    ///
    /// ```
    /// #![feature(const_type_id)]
    ///
    /// use std::any::TypeId;
    ///
    /// let typeof_string = TypeId::of::<String>();
    /// let typeof_bool = TypeId::of::<bool>();
    ///
    /// assert!(typeof_string.matches(typeof_string);
    /// assert!(!typeof_bool.matches(typeof_string);
    /// ```
    #[unstable(feature = "const_type_id", issue = "77125")]
    #[rustc_const_unstable(feature = "const_type_id", issue = "77125")]
    pub const fn matches(&self, other: TypeId) -> bool {
        self == other
    }
}

It relies on the standard library having some kind of support for const equality, without exposing what that support is. It decouples this pattern of specialization from const trait support or from full specialization.

Our example from before becomes:

const fn value_from_primitive<'a, T: ?Sized + 'static>(value: &'a T) -> Option<Value<'a>> {
    let id = TypeId::of::<T>();
    
    if id.matches(TypeId::of::<i32>()) {
        // Using `TypeId` instead of `Any::downcast` because `T` is unsized; so we can't convert into a `dyn Any`
        // without losing the `'static` bound
        // SAFETY: `T` has been asserted to be `i32`
        return Some(Value::I32(unsafe { *(value as *const T as *const i32) }))
    }

    // possibly many other branches...
    
    None
}

Alternatives

You could do this using plain-old ==, but that needs const trait support, which is still in its design stages. When equality does become possible in const contexts, this method is simply a semantic alternative to it that's also a good place to document what the implications of two TypeIds being equal or not are.

You could also use specialization, which isn't being actively pushed and has no clear path to stabilization.

You could use Any::is and Any::downcast_ref for this, but Any requires Sized + 'static (Sized for coercing to dyn Any and 'static from its trait bounds), which rules out unsized types like str.

Links and related work

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions