rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.85k stars 1.55k forks source link

Get type of an arbitrary expression #2704

Open nvzqz opened 5 years ago

nvzqz commented 5 years ago

It would be very useful in certain contexts (such as macros) to be able to get the concrete type of some variable through a given binding.

The idea for the syntax came from the upcoming .await syntax. Since this sets the precedent of having reserved keywords in a property position, I think that this would fit well. However, I understand that—unlike ? and .await.type acts more as an accessor instead of a control flow construct. Depending on one's point of view, this may be a good thing since it aligns somewhat with how property accessors appear today.

Examples

It would work as follows:

let x = 20;

// Equivalent to `typeof(x) y = x;` in C
let y: x.type = x;

type X = x.type;

static_assertions::assert_eq_type!(x.type, i32);

How it may be used within a macro:

macro_rules! do_stuff {
    ($x:expr) => {
        let y = <$x.type>::new(/* ... */);
        $x.do_stuff_with(y);
    }
}

Currently, the only way of getting the "type" of a binding within the context of a macro doesn't allow for the above code to work. For example:

macro_rules! do_stuff {
    ($x:expr) => {
        fn with_type_of<T>(x: T) {
            let y = T::new(/* ... */);
            x.do_stuff_with(y);
        }
        with_type_of($x);
    }
}

Prior art

In C, there's typeof(), which this feature would be an exact parallel to.

In Swift, there's type(of:), which returns a runtime value with metadata for the type of a value. This is different in that this proposed x.type would only be usable within a compile-time type context, not a runtime one.

For those who want to go spelunking into other languages, there's a typeof Wikipedia article.

Why I want this

It would make the implementation of assert_eq_size_val a bit less awkward.

I can also get more imaginative about use cases outside of the given examples upon request.

nvzqz commented 5 years ago

As @memoryruins pointed out to me, apparently typeof is already a reserved keyword as seen in issue-42060.rs and issue-29184.rs. However, I think the above proposal is a more elegant approach than C-style typeof().

comex commented 5 years ago

Interesting idea!

One obstacle might be parsing. Similarly to const generics, you have an expression appearing in type position without advance notice to the parser. With const generics, the current solution is to require {} around all expressions other than literals and identifiers... but transposing the same rule here, you‘d have to write things like {foo()}.type, which I think looks more awkward than just using typeof. On the other hand, the const generics rule is conservative, and it might be possible to relax it in the future to the point that you wouldn’t usually need braces; if so, the same could apply here.

nvzqz commented 5 years ago

You bring up a very good point. I suppose {expr}.type would be fine in the meantime but that is quite awkward. Do you know what work is being done right now to eliminate that from const generics?

comex commented 5 years ago

For const generics, I think everyone is waiting for the stuff that's already accepted to be implemented before proposing any extensions. (It's been a long wait, but thanks to varkor's work it seems pretty close to being ready!)

I'm also only speaking for myself when I say it might be possible to relax the rule. I think I've seen some talk of it in the past, but I don't remember where/when exactly, and I have no idea what anyone thinks of the idea now. shrug

nvzqz commented 5 years ago

In the macro example I gave, would that work without needing {$x}.type since the parser knows that the tokens are being treated as an expression?

jan-hudec commented 5 years ago

Regarding prior art, note

In C, there's typeof(), which this feature would be an exact parallel to.

That one is a GCC extension. However, C++ introduced it under the name decltype (which is more explicit in that it returns the declared and not the dynamic type—so it might be actually better name here too).

It was introduced because many template expressions in C++ return values where the type is either insanely complex, or the specification does not give it any name.

Rust uses generics even more than C++, so it also has all the same problems with the return values, and while the existential types cover many of the use-cases, it does not cover all of them.

uzytkownik commented 2 years ago

It was introduced because many template expressions in C++ return values where the type is either insanely complex, or the specification does not give it any name.

The latter is true for Rust as well, now that async fn is here (or lambdas). And however the async fn is nice I stumble into the issue here every time I try to do something more interesting with futures (like storing them into a structure) - I need to somehow name the type and the only way to do it currently is to box it. The only alternative is to write them manually without any lambdas and get 'insanely complex' type.

MithicSpirit commented 2 years ago

This is also useful for functional programming when the compiler can't figure out how a closure will be called and requests that the type of its arguments be manually specified. If these arguments are themselves closures, as is often the case with curried higher-order functions, then doing so currently requires the overhead of using dyn, even if it is known at compile-time (and even write-time) the exact closure that will be passed in.

From my experience, this (along with the lack of HKTs, which can be worked around with https://github.com/rust-lang/rust/issues/44265) makes proper functional programming quite a hassle in Rust.

akhilman commented 10 months ago

There is also std::result_of and std::invoke_result in C++ that could be handy to store results of functions with impl return types to structs.

Something like:

async fn make_foo() -> Value {
...
}
struct Bar {
    foo_future: result_of!(make_foo);
}
async {
    let foo_future = make_foo();
    let bar = Bar { foo_future };
}
momvart commented 2 months ago

Any updates on this?

I ran into the same problem when working with libafl that relies heavily on generics. So the exact types can get pretty long. Example of a type that needs to be explicitly passed:

CommandExecutor<
    (StdMapObserver<u8, false>, (SanitizerHashShmemObserver, ())),
    StdState<BytesInput, InMemoryCorpus<BytesInput>, RomuDuoJrRand, OnDiskCorpus<BytesInput>>,
    AdapterExecutor,
> 

which could imaginary be replaced with executor.type.

SOF3 commented 2 months ago

do we still need this with TAIT now?

Lokathor commented 2 months ago

Yes, traits lose info compared to a specific type.