[Pitch] Metatype

The metatype is the type of a type.

Here are some possible use cases of metatype in Rust:

  • Store a type in a variable
  • Perform pattern matching on a type
  • Use a type as the key of a HashMap

The grammar of metatype may like:

  • To get the metatype of a type: T::type, e.g. String::type, i32::type, Vec<i32>::type
  • To get the metatype from an instance: x.type, e.g. String::from("hello").type == String::type, 0i32.type == i32::type
  • The identifier of metatype: Type

An example of using type as the key of a HashMap:

let mut map = HashMap::<Type, i32>::new();
map.insert(String::type, 0);
map.insert(i32::type, 1);

In Swift, we can get the metatype of a type T by T.Type.

Maybe we should implement the metatype in Rust?

1 Like

Is this like a TypeId in std::any - Rust?

10 Likes

For a second I thought this was gonna be about adding higher kinded types to Rust by another name :sweat_smile: . Since "kind" is commonly described as "the type of a type".

3 Likes

Is this like a TypeId in std::any - Rust?

You can convert a type to TypeId, but cannot do it reversely...

For a second I thought this was gonna be about adding higher kinded types to Rust by another name :sweat_smile: . Since "kind" is commonly described as "the type of a type".

a kind is the type of a type constructor or, less commonly, the type of a higher-order type operator.

--- from "kind"

They are different, the metatype is only *, a kind can be *, * -> * or * -> * -> * ...

1 Like

This seems logical to me since at runtime types stop existing. What are the semantics you're looking for here?

5 Likes

What are we talking about here, something like this?

const T: Type = u32::type;
let foo: reify T = 0u32;

where the reify keyword turns a Type compile-time constant into a type.

2 Likes

It can be one of the use cases.

Here's another:

fn cast(any: Box<dyn Any>, T: Type) {
    if let Ok(x) = any.downcast::<T>() {
        // do something
    }
}

Having read some documentation about Swift metatypes, I'm not seeing anything remotely like the code in your response, which looks like "the type argument to downcast is a runtime value".

What I've seen is more like:

fn cast<T>(any: Box<dyn Any>, _: T::type) {
    if let Ok(x) = any.downcast::<T>() {
        // do something
    }
}

which can be emulated today with PhantomData :

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c8dedced03ba680ba704716340e98f04

use std::any::{self, Any};
use std::marker::PhantomData;

trait GetType {
    const TYPE: PhantomData<Self> = PhantomData;
}

impl<T: ?Sized> GetType for T {}

fn cast<T: Any>(any: Box<dyn Any>, _: PhantomData<T>) {
    if let Ok(_x) = any.downcast::<T>() {
        println!("downcasted into a {}", any::type_name::<T>());
    } else {
        println!("couldn't downcast into a {}", any::type_name::<T>());
    }
}

fn main(){
    cast(Box::new(100i32), u32::TYPE);
    cast(Box::new(100i32), i32::TYPE);
}

(I'd do cast::<u32>(Box::new(100i32)) instead, this is just for demonstration purposes)

2 Likes

And what do you do in // do something? You don't know anything about T. How does this differ from:

fn cast(any: Box<dyn Any>, type_id: TypeId) {
    if any.type_id() == type_id {
        // do something
    }
}

?

4 Likes

How will this interact with a type being generic over lifetimes?

For example, what should the following program print?

fn ref_to_type<'a>(r: &'a mut i32) -> Type {
    r.type
}

fn main() {
    let a = 5;

    let t1 = ref_to_type(&mut a);
    let t2 = ref_to_type(&mut a);

    println!("{}", t1 == t2);
}
Implications of each result

If the result is true, then this new Type type won't be able to be used to downcast anything with references.

Alternatively, if the result is false, we can use it to downcast, but then we have the trouble of keeping track of unique lifetimes and chasing them all through codegen (this is the same reason specialisation with lifetimes is very hard to get right)

[edit: removed extra spaces and code mistakes that came free with my phone's keyboard]

1 Like

But by this approach, we can't (for example) store PhantomData<T> (with different T) in a vector, and pass it around :thinking: like this:

let ts: Vec<Type> = Vec::new();
let bs: Vec<Box<dyn Any>> = Vec::new();
for (type_, box) in ts.zip(bs) {
    let x = cast(box, type_);
}

Only T: Any (following the same rule as fn downcast<T>(self), and excluding non-'static reference) can have a metatype.

So, what exactly is the semantics of cast(Box<dyn Any>, Type)? What would its type signature look like?

Rust currently has very minimal runtime type information, basically limited to just TypeId and the vtables you include via dyn Trait unsizing.

You're very casually proposing Rust to have some form of dependent types (making runtime values determine the type of something), without specifying any of the semantics of what that means.This isn't something you can just add to Rust without considering the semantics implications.

For example, you've proposed this to be valid code:

fn cast(any: Box<dyn Any>, T: Type) /* apparently this is supposed to return something */ {
    if let Ok(downcasted_x) = any.downcast::<T>() {
        // do something
    }
}

fn main(){
    let ts: Vec<Type> = Vec::new();
    let bs: Vec<Box<dyn Any>> = Vec::new();
    for (type_, boxed) in ts.zip(bs) {
        let x = cast(boxed, type_);
    }
}

My interpretation of this is that T is a runtime value that determines the static type argument to the downcast method.

So I have some questions:

  • What's the type of downcasted_x? As in, the type you can write in let _: here = downcasted_x; such that the code compiles.
  • Can you use T as the type of a local variable or the return type of the function?
  • What methods can be called on downcasted_x?

Assuming that Type runtime variables can be used as the static type of a variable, is this a compile-time type error?

fn types(foo: Type, bar: Type, baz: foo) -> bar {
     baz
}

since the above code is the runtime-type equivalent of this generic function:

fn types<foo, bar>(baz: foo) -> bar {
     baz
}

which errors at definition time (which means that it errors even without any callers) because foo isn't proven to be the same type as bar for all possible invocations.

I'm leaving other questions for later, this is just the basics to understand as a user of the language what you're proposing even is.

2 Likes

Those aren't use cases. They're descriptions of things that a "metatype" would support, but not descriptions of a problem that doing so would solve.

Can you talk more about what problems you are trying to solve for which these capabilities would help you?

12 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.