Idea: Change type ascription syntax from `:` to `type` keyword

Currently the type ascription feature uses the : syntax. It’s on nightly for quite a long time, but it doesn’t look it’s going to be stabilized soon. I guess that one of the problems with the : syntax is its interoperation with struct literals, which also use the same symbol. So I’d like to propose to change the colon symbol to type to avoid this amiguity. I’ve tried to search whether it was discussed before, but I haven’t found anything. So if somebody has some links to previous discussion on this syntax, I’d be happy to see them.

Motivation

  • Less ambiguity with the struct literal syntax
  • More compatibility with various “anonymous structs” / struct shorthand syntax / keyword arguments proposals (by freeing the :).
  • Ability to use type ascriptions in patterns in an unambiguous way (I hope).

Examples

Type ascription of simple expression:

let foo = (0..10).collect() type Vec<_>;

Struct literals:

Struct { foo: foo type T } // simply annotating expression
Struct { foo type T } // shorthand syntax
Struct { foo type T: foo } // perhaps we should also allow this, for consistency?

Interaction with -> T-like functions (imho it reads pretty well):

let x = line.parse()? type i32;
let x = height.into() type f64;

Patterns:

match "aaa".parse() {
    Ok(a type T) => { ... }
    Err(Error { inner: e type E }) => { ... }
}

Detailed design

For expressions, use the same grammar as the as-conversion. For patterns, not sure about the precise rules yet.

Drawbacks

  • Longer syntax (one may say it’s less perl-y though)
  • Perhaps it’s less discoverable? (although more searchable)
  • If we allow type in all patterns, there will be multiple ways to write an annotated let statement:
let a: T = foo();
let a = foo() type T;
let a type T = foo();
// Also
let b type Result<T, _>: Result<_, E> = foo();

(I guess that allowing current type ascription syntax – : – in patterns whould just change the let statement grammar to let <pat> [ = <expr>])

  • Probably, this also introduces ambiguity in grammar, which I just haven’t thought about yet.
6 Likes

I’d prefer the additional type keyword following : instead of type replacing :.

let a: type T = foo();
let a = foo() : type T;

fn bar( arg: type T1) {}

In this way, the type keyword actually plays the role of typename keyword in C++.

Less ambiguity with the struct literal syntax

I think in the type vs struct literal collision, struct is at fault. Everywhere else in the language it's binding: type, but in struct literals it's binding: value.

So if any such huge late change is going to be made, I suggest fixing struct literal syntax instead. It could be same as C:

StructName {
   .field_name = value,
   .other_field = value,
}

This will remove the problematic type/value confusion from the syntax. It also makes it more greppable for .field_name = value which finds both obj.field_name = value.

12 Likes

Syntax is not the reason why the feature is in limbo (also there’s no conflict with struct literals in the current grammar).
The reason is that the feature is under-implemented (coercions) and is not generally useful enough.

Implementing coercions (while keeping soundness) will certainly move the feature closer to the final decision.
Implementing type ascriptions in patterns as an experiment on nightly will also move things in the right direction, I think.

Sources:

6 Likes

IMO the motivation section of the type ascription RFC is really weak, and since it still hasn’t been stabilized after all this time, it seems to me like Rust works just fine without it. It’s at best a slight convenience in some cases, whereas it restricts the design space for other features (e.g. named arguments). Can’t we just scrap type ascription altogether?

1 Like

That being said, I like this version much, much better than the current syntax, and I’d definitely prefer type over the :.

If we can, I’m strongly in favor of keeping the : symbol here. Notably, this was not a symbol chosen at random and comes from typing judgements in type theory and lambda calculus. https://ncatlab.org/nlab/show/judgment, https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus#Typing_rules This notation is also used by a variety of functional languages.

Also; I’d prefer it if we did not scrap type ascription. To me, it provides a lightweight syntactic mechanism to guide and constrain type inference without turbofish or let bindings and can improve writing flow.

In my view, <expr> type <type> does not read well either while <expr> typed as <type> would, but that is simply too long and verbose.

5 Likes

Nooo! :cry: It's the only solution we have been promised to the Into/AsRef problem!... a problem which is only going to become more and more prominent over time:

The Into/AsRef problem
  • The author of crate bbbb, being the model Rustacean that he is, writes impl From<aaaa::A> for B rather than having from_a method.
  • The author of crate cccc, being the model Rustacean that he is, writes impl From<bbbb::B> for C rather than having from_b method.
  • You call a function that returns an aaaa::A...
  • ...and want to return a cccc::C.
  • Obviously, func().into().into() will not work.
  • Have fun.
4 Likes

There are several working solutions to this.

let x: B = func().into()
x.into()
<A as Into<B>>::into(func()).into()

The first especially doesn not seem worse than this:

(func().into(): B).into()
3 Likes

Maybe we could just have a generic always-inline identity function that returns its argument, and use that for type ascription with no language changes:

id::<B>(x.into()).into()

Only difference seems to be that it doesn’t implicitly work for non-movable lvalues, but you need to borrow and dereference explicitly.

2 Likes

I am uncertain whether this actually works, but I thought this was the exact thing the turbofish was for?

func().into::<B>().into()

It’s also possible that type inference will evolve to better handle method calls.

Turbo fish like this doesn't work in this particular instance because the parameter is on trait Into<T> not fn into(self). But there are several other solutions that don't require this type ascription feature.

2 Likes

One may also consider the following syntax for type ascription (indentation is not a problem, as ascription is never needed at the top level anyway):

Toto::from(frobnicate(y).into())
//:        ~~~~~~~~~~~~~
//:        has type u8

It’s a bit awkward to input, but seems like a real gain in terms of legibility.

1 Like

I prefer to use from in cases where inference gets confused: C::from(B::from(func()). It doesn’t have the nice method call ordering but it’s rare to have more than one layer of them in my experience, and it matches my preferred String::from as well.

6 Likes

Isn’t struct literal syntax stable? In that case, it’s not going to change.

(And, to be honest, I don’t like that C-style syntax, it’s harder to type (= is less convenient to access on the keyboard than :) and harder to read too.)

Can’t we just scrap type ascription altogether?

This! I've been programming in Rust so far without once needing to touch type ascriptions. Turbofish and binding annotation are more than enough. Also ascriptions are ug-ly.

Isn’t struct literal syntax stable? In that case, it’s not going to change.

I think it can change over an epoch (that's the point of epochs), but only if it is a really, really big improvement over the current arrangement.

This looks like the diamond problem.

I think it can change over an epoch

Oh, that's right.

but only if it is a really, really big improvement over the current arrangement.

Sure — which I don't think it is.

This also frustrates me when refactoring - moving let bindings into a struct for example. Have to change all the symbols. And it’s the syntax most functional languages use for fields.

I remember @nrc actually tried to propose changing the struct syntax pre-1.0 - would be cool to see it come up again as something to change across epochs, but I’m not sure it would have enough drive to get through.

5 Likes