Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get type of an arbitrary expression #2706

Open
wants to merge 2 commits into
base: master
from
Open
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -0,0 +1,239 @@
- Feature Name: `get_expr_type`
- Start Date: 2019-05-30
- RFC PR: [rust-lang/rfcs#2706](https://github.com/rust-lang/rfcs/pull/2706)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Add the ability to retrieve the concrete type of an arbitrary expression so that
it may be reused by other code that relies on the expression.

# Motivation
[motivation]: #motivation

> "Macros will become more powerful than you can possibly imagine."
>
> – Obi-Wan Kenobi (_supposedly_)
Within the context of a macro, this feature would be very useful.

It would allow for:

- Defining a function that takes the concrete type of a given expression.
- Calling upon type-level functions based upon a given expression.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Given some value `x`, we can retrieve its type with just `x.type`. This type can
then be used within various contexts:

```rust
let x = "hellooo";
let y: x.type = x; // C equivalent: typeof(x) y = x;
type X = x.type;
assert_eq_type!(X, &str); // taken from `static_assertions`
assert!(<x.type>::default().is_empty());
```

Note that if the expression resolves to a long operation, the operation will
_not_ be evaluated.

```rust
type T = do_stuff().do_more().type;
```

This feature is especially useful within macros when wanting to generate
functions or types based off of the given expression.

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

When we get `x.type` here, it is no different than just substituting it directly
with `Value`. This allows for accessing all type-specific functionality.

This isn't possible with the current mechanism for getting the type of an
expression:

```rust
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);
}
}
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

I am uncertain of how this would be implemented within the compiler but I
imagine that it would leverage the work that's already been done with `const`
generics.

Some cases to be aware of are non-simple expressions, such as `1 + 1`. It is

This comment has been minimized.

Copy link
@wesleywiser

wesleywiser May 31, 2019

Member

How does this interact with type inference? Currently, as I understand it, Rust allows the following code:

let mut v = Vec::new();  //1, 3
v.push(1u8); //2

Where type inference works in the following way:

  1. v has type Vec<?>
  2. Vec::push() is called with a u8. This parameter is of type T therefore T has type u8
  3. Therefore v has type Vec<u8>.

Now consider the following code:

let mut v1 = Vec::new();
type V = v1.type; //What is the type `V` here? 
let mut v2 = V::new();

v2.push(1u16); //Does this line mean `v1` is of type `Vec<u16>`?
v1.push(1u8); //Does this line compile?

Would you expect this code to be valid? If not, what is the type of v1? Is it Vec<u8> because of v1.push(1u8) or Vec<u16> because type V = v1.type and v2.push(1u16)?

This comment has been minimized.

Copy link
@Centril

Centril May 31, 2019

Member

Simplifying a bit:

let mut v1 = Vec::new();
let mut v2 = <v1.type>::new();

What should happen with v2 is that the known type of v1 at that point is used. Specifically, we know that v1: Vec<?T0> and so this type, including the names of unsolved inference variables, is also assigned to v2. This would use the same inference variable ?T0 for both v1 and v2. Therefore, when we do v2.push(1u16) we should equate ?T0 = u16 and therefore it follows that v1: Vec<u16> wherefore v1.push(1u18); cannot compile.

This comment has been minimized.

Copy link
@wesleywiser

wesleywiser May 31, 2019

Member

Seems logical but potentially surprising to users that v1's type was inferred because of how v2 was used.

This comment has been minimized.

Copy link
@Centril

Centril May 31, 2019

Member

That's type inference for you... often surprising ;) E.g. we have bidirectional type-checking but folks often don't notice the difference.

We could do some alternative schemes:

  1. Generate fresh variables in expr.type for each variable in expr.type.
    Seems less useful?

  2. Error when unsolved variables are found in expr.type.
    This entails that the full type must be known in expr.

This comment has been minimized.

Copy link
@ExpHP

ExpHP Jun 7, 2019

This isn't surprising at all. There are already many, many ways to crate a situation in rust where the type of some local v1 is decided based on how another local v2 is used.

understood that this has a type of `i32` by default, but such an expression may
be difficult to parse in a generic context.

The following may only be possible in this form:

```rust
do_stuff::<{1 + 1}.type>();
```

See [unresolved questions](#unresolved-questions) for more regarding the above
example.

# Drawbacks
[drawbacks]: #drawbacks

By using a postfix syntax, one may write a long expression only to realize that
nothing before the `.type` part will actually not be evaluated. However, such
code would be in a context where it's obvious that the expression isn't
important.

```rust
do_thing::<send_request().await?.body().await?.contents.type>();
```

To be fair, this isn't the [weirdest thing in Rust](https://github.com/rust-lang/rust/blob/master/src/test/run-pass/weird-exprs.rs).

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

Retrieving the type via the overloaded `.` operator feels like a natural
extension of the language.

As of this writing, the lang team [has come to a final
decision](https://boats.gitlab.io/blog/post/await-decision-ii/) regarding the
syntax for `await`, and that is to make it a postfix operation: `expr.await`.

This comment has been minimized.

Copy link
@kennytm

kennytm Jun 4, 2019

Member

The reason .await was chosen is because we can chain stuff after the .await. Similarly, .match is an operation which takes an expression and returns an expression, allowing chaining.

There is no such advantage for a type:

  • &'a X, *const X are prefix operators
  • [X], (X, Y) are circumfix
  • You cannot write foo.type::Stuff since foo.type isn't a path, so you'll need <foo.type>::Stuff (or drastically change the grammar), making associated type another circumfix operator.

So I don't see any reason why x.type is picked instead of typeof(x) (or typeof x to match impl Trait/dyn Trait).

This comment has been minimized.

Copy link
@nvzqz

nvzqz Jun 4, 2019

Author

I do prefer the syntax <typeof x>::Stuff over <x.type>::Stuff and so I may change the proposal to use this instead. It's less visual noise in my opinion.

This sets a precedent of allowing reserved keywords in what would otherwise be
the position for a property access.

## Alternatives

- A `type_of!` macro:

```rust
type T = type_of!(expr);
```

However, this would be more awkward when used in a generic context:

```rust
type V = Vec<type_of!(elem)>;
```

Depending on current parser limitations, it may need to be surrounded by
braces when in a generic context.

- A freestanding magical `typeof()` "function" in the same style as in C:

```rust
type T = typeof(expr);
```

The `typeof` identifier is already reserved as a keyword and so placing it into
the `std`/`core` prelude would not be a breaking change.

# Prior art
[prior-art]: #prior-art

## C

As a compiler extension, GCC allows for getting the type of an expression via
[`typeof()`](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html). This is often used
within macros to create intermediate bindings for inputs so as to not evaluate
inputs more than once.

For example, a safe `max` macro that evaluates its arguments exactly once can be
defined as:

```c
#define max(a, b) \
({ typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; })
```
Of course, with Rust's type inference, this same workaround isn't necessary.
## Swift
Swift's `type(of:)` produces runtime metatype value, upon which type-level
functions can be called.
```swift
let value = "Hola" // String
let valueType = type(of: value) // String.Type
let other = valueType.init(["H", "o", "l", "a"]) // String
assert(value == other)
```
## Other Languages
Many other languages include a `typeof` feature, as can be seen in
[this Wikipedia article](https://en.wikipedia.org/wiki/Typeof).
# Unresolved questions
[unresolved-questions]: #unresolved-questions
- An issue that `const` generics deals with is needing to wrap the expression in
braces (e.g. `{expr}`).
What situations would require the expression before `.type` to be wrapped in
braces as well?
Would this always be necessary in the context of expressions more complicated
than providing one without spaces? (e.g. `f::<{a + b}.type>()`)
- Would an expression provided within a macro as a `:expr` be exempt from the
above requirement?
```rust
macro_rules! call_f {
($x:expr) => { f::<$x.type>() }
}
call_f!(2 + 2);
```
This works within the context of `const` generics and so one can imagine that
it would also work here.
- Would the following work?
```rust
type T = (2 + 2).type;
```
Or would it be restricted to:
```rust
type T = {2 + 2}.type;
```
# Future possibilities
[future-possibilities]: #future-possibilities
This can push forward the capabilities of macros greatly and make them on-par
with C++ templates.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.