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

Conversation

Projects
None yet
@nvzqz
Copy link

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.

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.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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.

@KrishnaSannasi

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

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.

@matthieu-m

This comment has been minimized.

Copy link

commented Jun 5, 2019

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

This comment has been minimized.

Copy link
Contributor

commented Jun 6, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

commented Jun 6, 2019

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

This comment has been minimized.

Copy link

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

@KrishnaSannasi

This comment has been minimized.

Copy link

commented Jun 7, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.