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

.type to get expression type #2706

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions text/0000-get-expr-type.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this interact with trait objects where the concrete type is not known at compile time?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dyn Trait is the type. Also dyn Trait reveals only what Trait permits about original type, although some traits like Any or Error provide downcast or type_id methods.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is dyn Trait considered a concrete type? I would have expected the underlying type to be. Either it returns dyn Trait or is disallowed if the underlying type cannot be known at compile time - either way I think it deserves mention in the RFC.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've no idea if concrete means anything. dyn Trait is an unsized type with a representation expressed in terms of pointer metadata, so vaguely like [T].


```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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

@Centril Centril May 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.