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

Conversation

nvzqz
Copy link
Contributor

@nvzqz nvzqz commented May 30, 2019

This proposal adds the ability to get the type of an arbitrary expression via expr.type.

It would work as follows:

let x = 20;
let y: x.type = x; // C equivalent: typeof(x) y = x;

type X = x.type;

assert_eq_type!(X, i32); // taken from `static_assertions`

The type identifier is already reserved as a keyword and so this isn't a breaking change.

This is a proper RFC based off of #2704.

@Centril Centril added T-lang Relevant to the language team, which will review and decide on the RFC. A-expressions Term language related proposals & ideas A-typesystem Type system related proposals & ideas A-syntax Syntax related proposals & ideas labels May 30, 2019
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.

@mark-i-m
Copy link
Member

mark-i-m commented May 31, 2019

I’m not a fan of this kind of type definition. Currently you can reason about the high-level structure of code purely by looking at function signatures and type definitions. But with this change the type of something can require reading the implementation to figure out. It breaks an abstraction boundary in a way the decreases readability.

I do sympathize with the motivation of using it in macros, though (it would massively clean up some macros).

Perhaps a more restricted alternative would be to syntactically expose inference variables:

let x: Vec<?T> = Vec::new(); // ?T is a named inference variable 
let y: ?T = 0;

This has a few benefits:

  • Doesn’t allow expressions in types syntactically
  • Uses existing type inference machinery in the compiler. ?T already exists, but is currently an implementation detail. EDIT: to clarify, I think the existing machinery is sufficient; we wouldn’t need to build anything else.
  • Solves the use case, I think
  • IMHO is more readable

I would still want it to be explicitly bad style to use this anywhere except macros, though.

@burdges
Copy link

burdges commented May 31, 2019

In principle you can always achieve this by introducing some nested function, right?

fn same_type<T>(x: T, y: Vec<T>) -> (T,Vec<T>) { (x, y) }

It even sounds possible to build these with a proc macro, although not sure how that interacts whit macro evaluation?

@nvzqz
Copy link
Contributor Author

nvzqz commented Jun 1, 2019

@mark-i-m

But with this change the type of something can require reading the implementation to figure out. It breaks an abstraction boundary in a way the decreases readability.

I wholeheartedly agree that this can lead to code that's very difficult to read and thus a headache to maintain.

Perhaps a more restricted alternative would be to syntactically expose inference variables:

let x: Vec<?T> = Vec::new(); // ?T is a named inference variable 
let y: ?T = 0;

This has a few benefits:

  • Doesn’t allow expressions in types syntactically
  • Uses existing type inference machinery in the compiler. ?T already exists, but is currently an implementation detail. EDIT: to clarify, I think the existing machinery is sufficient; we wouldn’t need to build anything else.
  • Solves the use case, I think
  • IMHO is more readable

I like this approach for some of the reasons you listed.

How would I be able to make a type alias for type reuse in greater contexts?

Also, this approach would require evaluating an expression or doing something weird in a macro to work blindly over the type of an expression.

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

Something important that an implementor of this feature would need to consider is macro hygiene with respect to inference types used within macros. If a ?T is already used in the scope of do_stuff!'s call site, it should not conflict with this one. However, if there is a ?T in use in the scope of do_stuff!'s declaration site, it should be assumed that they refer to the same type.

I would still want it to be explicitly bad style to use this anywhere except macros, though.

Would this be done within the compiler directly or as a clippy lint?


@burdges

I don't see how this feature is achievable with generics or a procedural macro. Generics erase all concrete type information that can then only be added with traits. Also, if you can't do this right now in a normal macro, how would this be achieved with a procedural macro? Both are simply forms of token substitution.

@burdges
Copy link

burdges commented Jun 1, 2019

I think the macro invocation would look like

bind_types!('a,T,S; x: &'a, mut T, mut y: Vec<S>, z: (T,T,S));

which then create code like

fn do_bind_types_473<'a,T,S>(x: &'a mut T, y: Vec<S>, z: (T,T,S)) -> (&'a mut T, Vec<T>, (T,T,S)) { (x, y, z) }
let (x, mut y, z) = do_bind_types_473(x,y,z);

It merely makes the inference variable a type parameter of some nested function.

@mark-i-m
Copy link
Member

mark-i-m commented Jun 1, 2019

Also, this approach would require evaluating an expression or doing something weird in a macro to work blindly over the type of an expression.

True, but I don’t think this is more weird than using an expression in a type. Also, macros already do some weird/awkward things like passing the types in as meta-variables. I suppose this is a personal judgement.

How would I be able to make a type alias for type reuse in greater contexts?

I propose that one wouldn’t do that (at least for now). Would it still solve most of the use case to only be able to use this inside of a function body? It seems like normal generics can cover most of the other use cases, right?

Something important that an implementor of this feature would need to consider is macro hygiene with respect to inference types used within macros.

I imagine it would follow the dame rules as normal generics, right? Is there an additional challenge here?

Would this be done within the compiler directly or as a clippy lint?

Clippy is probably more appropriate, IMHO.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 1, 2019

How would I be able to make a type alias for type reuse in greater contexts?

I propose that one wouldn’t do that (at least for now). Would it still solve most of the use case to only be able to use this inside of a function body? It seems like normal generics can cover most of the other use cases, right?

My understanding was that the general problem of "naming the unnameable" types is supposed to be solved by impl Trait extensions, especially the existential types proposed and accepted in #2071. IIUC, much of this is also required to make "async methods in traits" a thing, so there's plenty of motivation for it to get done.


Personally, I don't think we can really evaluate the usefulness of a typeof-like feature until more of those impl Trait extensions get finalized. Every typeof-like proposal I've seen either introduces very non-obvious questions around how typeofs affect type inference (like this RFC), or introduces very non-obvious questions around the arcane type syntax one would want to pass to typeof (just like C, but unlike this RFC). It seems clear that any typeof feature (which is more than just sugar over already accepted impl Trait extensions and already well-understood tricks like @burdges's same_type) would likely introduce significant new complexity to the type system, and I think that requires clearer and stronger motivation than we currently have.

Unless I'm missing something huge, the motivation for the RFC seems really thin. There's no example of code that's impossible today yet possible with this feature, only examples of code that would be very slightly easier to write with this feature. If the goal is indeed to make already fairly straightforward code easier to write, we should be talking about sugars, not type system extensions. If we're actually talking about code that is painfully convoluted today, yet easy with this feature, then I like @mark-i-m's idea of exposing inference variables (it's "heavier" than a mere sugar, but it's definitely not a true type system extension either), but the RFC needs some examples that clearly show the alleged pain and gain.

@comex
Copy link

comex commented Jun 1, 2019

Exposing inference variables would have the significant drawback of not working outside of a function – yet outside of a function is precisely where this feature would be most powerful, since within a function you can already take advantage of inference to achieve similar results in many cases. For example, the <$x.type>::new example isn't possible today, but if you allow Default::default as an (arguably more principled) alternative, you can use a helper like:

fn default_of_same_type_as<T: Default>(_: &T) -> T { T::default() }

Personally, I've wished for typeof when trying to design a Rust equivalent to construct, a Python library for decoding/encoding binary formats. My basic idea was that a macro would transform something like:

struct Foo {
	count: u32 = beu32, // big endian u32
	items: Vec<u32> = repeat(count, beu32),
}

to:

struct Foo {
    count: u32,
    items: Vec<u32>,
}
impl Decodable for Foo {
    fn decode(stream: &mut Stream) -> Foo {
        let count: u32 = beu32.decode(stream);
        let items: Vec<u32> = repeat(count, beu32).decode(stream);
        Foo { count, items }
    }
}

(Plus maybe an Encodable implementation as well, though there are some complexities there. But anyway...)

Problem is, a lot of structs have many fields of simple types:

struct Bar {
    a: u32 = beu32,
    b: u32 = beu32,
    c: u32 = beu32,
    d: u16 = beu16,
    e: u32 = beu32,
    // ...
}

In this case, it can get annoying to have to effectively write each field's type twice. I'd like to allow the user to omit the type and just write a = beu32. But that would require the struct definition to be something like

struct Bar {
    a: <typeof(beu32) as Decodable>::Output,
    // etc...
}

Of course, an alternative in this case would be to have the user represent how to decode/encode the field by writing a type, like BEU32. Then it would just be <BEU32 as Decodable>::Output. But in the more complex example above, repeat(count, beu32) clearly needs to be an expression, because count isn't even a constant. And I'd rather not need two different ways to represent the same thing.

@RustyYato
Copy link

RustyYato commented Jun 2, 2019

I think much of this can be solved with existential_type tracking issue, RFC, if it got extended a bit.

@burdges
Copy link

burdges commented Jun 2, 2019

We've also had the proposal for an f::Output associated type for methods f. If that existed, then you'd have type level expressions like X::<Y>::f::<Z>::Output, which should achieve the same results as this, but using only type level notation. It's not clear what makes things easier in practice.


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.

@matthieu-m
Copy link

For the "other languages" section, you can add C++ decltype(<expr>), which is standard, unlike GCC's typeof extension for C.

For the purposes of decltype, and other unevaluated contexts, C++ also features std::declval<X>() which "creates" a variable of type X out of thin air. It is useful when provided with a callable foo if you wish to know what is the type of foo(std::declval<X>()) without having an instance of X on hand.


Syntax-wise, I would prefer a syntax which made it really clear from the beginning of the expression that it is unevaluated; if you really need to go the unevaluated way. It's jarring to discover a function call wasn't a call only at the end.

@kornelski
Copy link
Contributor

Just from syntax perspective: please don't put expressions where types can appear. Mixing these two in the syntax tends to backfire later (with things like turbofish and struct literal parsing difficulties). Prefixing of the expression with a keyword/sigil would disambiguate it.

@burdges
Copy link

burdges commented Jun 6, 2019

It appears the named existential type RFC #2071 being accepted makes this RFC redundant. If not, this RFC should explain the difference.

You can always write roughly:

existential type Foo<T>: SomeTrait;
fn my_expr<T>(..) {
    expr : Foo<T>;
}

but named existential type better supports the common cases where either (a) some type constraint appear outside expressions, or (b) the expression is actually evaluated.

@gnzlbg
Copy link
Contributor

gnzlbg commented Jun 6, 2019

@matthieu-m

Syntax-wise, I would prefer a syntax which made it really clear from the beginning of the expression that it is unevaluated; if you really need to go the unevaluated way. It's jarring to discover a function call wasn't a call only at the end.

You mean unevaluated "at runtime", right? Finding the type of an expression does often require constant evaluation, e.g.,

const fn add(x: usize, y: usize) -> usize { x + y } 
pub type Foo = ([0_i32; {add(1, usize::max_value())}]).type;

does require evaluating add(1, usize::max_value()) at compile-time, evaluation can fail (in this case due to a panic! on overflow), etc.

Rust is not C++, but in C++ one often needs to be careful with decltype due to how a failed constant evaluation interacts with other parts of the language like SFINAE.

@matthieu-m
Copy link

Syntax-wise, I would prefer a syntax which made it really clear from the beginning of the expression that it is unevaluated; if you really need to go the unevaluated way. It's jarring to discover a function call wasn't a call only at the end.

You mean unevaluated "at runtime", right? Finding the type of an expression does often require constant evaluation, e.g.,

Yes, exactly. I am not yet used to compile-time evaluation in Rust.

@comex
Copy link

comex commented Jun 7, 2019

The existential type feature doesn’t make my use case redundant, because I would need to be able to fully access the type, rather than having allowed operations limited to some trait bound known a priori (which I don’t have).

@RustyYato
Copy link

@comex that is why I said if existential types got extended a bit

@burges The difference is that this proposal gets you to concrete type, which you can use to call inherent associated functions. Not something you can do with existential types, they must always go through a trait.

@nikomatsakis
Copy link
Contributor

@rust-lang/lang discussed this in today's "Backlog Bonanza" meeting. There was definite sympathy for the goals of this RFC -- a few of us were definitely missing the ability to name types, especially the "less nameable" types like those returned by impl Trait functions. However, overall we felt that the time was not ripe to pursue this. It's not a good fit for the 2020 roadmap priorities (basically: finishing up existing work, not adding new work) nor the lang team priorities we've enumerated. Moreover, we think it'd be good to pursue the existing impl Trait support towards conclusion first before pursuing the idea of typeof expressions.

@rfcbot fcp close

@rfcbot
Copy link
Collaborator

rfcbot commented Sep 2, 2020

Team member @nikomatsakis has proposed to close this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-close This RFC is in PFCP or FCP with a disposition to close it. labels Sep 2, 2020
@rfcbot rfcbot added the final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Sep 16, 2020
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 16, 2020

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot removed the proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. label Sep 16, 2020
```

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

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Sep 26, 2020
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 26, 2020

The final comment period, with a disposition to close, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC is now closed.

@rfcbot rfcbot added to-announce closed This FCP has been closed (as opposed to postponed) and removed disposition-close This RFC is in PFCP or FCP with a disposition to close it. labels Sep 26, 2020
@rfcbot rfcbot closed this Sep 26, 2020
@nvzqz
Copy link
Contributor Author

nvzqz commented Oct 17, 2021

@nikomatsakis thoughts on reopening this?

@nvzqz nvzqz changed the title Get type of an arbitrary expression .type to get expression type Oct 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas closed This FCP has been closed (as opposed to postponed) finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.