Stream: t-lang

Topic: type ascription


nikomatsakis (Oct 19 2020 at 22:07, on Zulip):

Dear @T-lang, I'd like to take people's temperature on type ascription syntax. I just re-read rfc#2623 and I was thinking that it might actually suggest a decent implementation path. So... are there outstanding concerns with x: T as type ascription syntax?

I'm going to try a poll!

nikomatsakis (Oct 19 2020 at 22:07, on Zulip):

/poll Type ascription syntax: what say you?

simulacrum (Oct 19 2020 at 22:20, on Zulip):

I still feel mildly uncomfortable wrt to the effects type ascription has on readability etc -- in particular, it seems to require the (expr): foo pattern and given .await as something that has landed in the meantime, it seems like the wrong thing. IIRC, there's some parser recovery problems with it too.

I have not read the RFC in detail, but I personally am unconvinced that type ascription is necessary. I sometimes want it, but often the extra let temporary wouldn't really hurt (and may even help).

boats (Oct 19 2020 at 22:36, on Zulip):

I don't think type ascription is very often useful and I'd prefer to save the syntax for keyword args

scottmcm (Oct 20 2020 at 00:18, on Zulip):

I feel with let x: T existing it's hard to pick any other syntax.

That said, I've pondered making x as T be ascription in an edition change. We're getting closer and closer to having everything it can do have more targeted library methods instead (.cast() on pointers, transmute!, .into(), etc). So that would potentially open up that syntax instead.

Josh Triplett (Oct 20 2020 at 00:22, on Zulip):

At the very least, we could have a lint for "you used as for something that isn't a simple ascription", and use an edition to ramp it to warn.

scottmcm (Oct 20 2020 at 00:29, on Zulip):

clippy has this one, though it's allow-by-default right now:

warning: using a potentially dangerous silent `as` conversion
 --> src/main.rs:3:13
  |
3 |     let _ = 123_i32 as u64;
  |             ^^^^^^^^^^^^^^
  |
  = help: consider using a safe wrapper for this conversion
scottmcm (Oct 20 2020 at 00:31, on Zulip):

I should go push on that WrappingFrom RFC...

Mario Carneiro (Oct 20 2020 at 02:17, on Zulip):

I guess it's too late to suggest the solution used in lean, which is to make the parentheses in (e: T) a mandatory part of the syntax

Mario Carneiro (Oct 20 2020 at 02:20, on Zulip):

I use type ascription all the time in lean and miss it dearly in rust. But in particular it is definitely used to trigger coercions, in fact that's the main reason to put them in, and the RFC seems like it's going to kill non-trivial use of the type ascription operator

Mario Carneiro (Oct 20 2020 at 02:29, on Zulip):

I would be happy for e as T to take on the role of type ascription too. But clippy has an annoying lint for this: if you write e as T when you should have written e: T (or maybe coerce e? what the hell am I supposed to write!), it gives a warning saying you should write explicit temporaries, which is silly, as it makes coercion more verbose than just calling a function

Mario Carneiro (Oct 20 2020 at 02:36, on Zulip):

By the way a side benefit of mandatory parentheses is that it solves all the problems around associativity, given all the prefix and postfix operators in rust. It seems good to be explicit if you are type-ascribing an expression to say which part you are talking about

Josh Triplett (Oct 20 2020 at 06:33, on Zulip):

Half the time I would want ascription, I'm at the end of an expression and having to wrap it in parens would be annoying.

Mario Carneiro (Oct 20 2020 at 06:45, on Zulip):

why at the end of an expression? Use cases differ across languages, but I often find myself using type ascription in lean to key the type so I can call a method, i.e. (e: T).method()

Mario Carneiro (Oct 20 2020 at 06:47, on Zulip):

in rust it's mostly when I want an explicit coercion, although occasionally it's more ergonomic than supplying type parameters (and I guess that's the use case most here are thinking about)

Mario Carneiro (Oct 20 2020 at 06:57, on Zulip):

If you are at the end of an expression, it seems like you would almost always have a place to put the type: if it is let x = expr; you can put the type on x instead of expr and if it's a return expression then the outer context is supplying the type. But when you are in the middle of an expression there isn't any place to put the type, especially if you are invoking a coercion because those have no syntax

Josh Triplett (Oct 20 2020 at 07:05, on Zulip):

End of an expression, not end of a statement.

Josh Triplett (Oct 20 2020 at 07:06, on Zulip):

Cases where I want ascription include return-type inference within a chain, for instance.

Josh Triplett (Oct 20 2020 at 07:07, on Zulip):

Forced parentheses have the same problem that try! did: it piles up on the front of the expression.

Josh Triplett (Oct 20 2020 at 07:09, on Zulip):

Suppose I want to write A::b(...).c(...).d(...).e(...), except I need ascription in one or two places before a call.

Josh Triplett (Oct 20 2020 at 07:10, on Zulip):

I would have to go back to the start of the chain and add a(nother) layer of parens.

Mario Carneiro (Oct 20 2020 at 07:13, on Zulip):

I don't see how you can do better than this with infix e : T or e as T though

Mario Carneiro (Oct 20 2020 at 07:13, on Zulip):

if it was e.ascr(T) then ok

Mario Carneiro (Oct 20 2020 at 07:14, on Zulip):

but that's kind of weird

Mario Carneiro (Oct 20 2020 at 07:15, on Zulip):

I think it's easier to use let in that situation though because it's very linear

Mario Carneiro (Oct 20 2020 at 07:15, on Zulip):

it's more annoying to use let when it turns your expression tree inside out

Josh Triplett (Oct 20 2020 at 07:16, on Zulip):

... That gives me an idea...

Josh Triplett (Oct 20 2020 at 07:16, on Zulip):

/me tries something on the playground.

Vadim Petrochenkov (Oct 20 2020 at 07:31, on Zulip):

Mario Carneiro said:

if it was e.ascr(T) then ok

e.as(T)?

Mario Carneiro (Oct 20 2020 at 07:31, on Zulip):

is this the same as e as T, semantically?

Mario Carneiro (Oct 20 2020 at 07:32, on Zulip):

or is it only coercion

Vadim Petrochenkov (Oct 20 2020 at 07:33, on Zulip):

I'm just suggesting a syntax without chaining problems here, not semantics.

Vadim Petrochenkov (Oct 20 2020 at 07:33, on Zulip):

But there was a suggestion to turn as into a type ascription above.

Josh Triplett (Oct 20 2020 at 07:33, on Zulip):

Couldn't get this to work:

trait Ascribe {
    fn ascribe(self) -> Self where Self: Sized { self }
}

impl<T: Sized> Ascribe for T {}

fn main() {
    let x = "foo".into().ascribe::<String>();
    dbg!(x);
}
Josh Triplett (Oct 20 2020 at 07:34, on Zulip):

Would be interesting if it were possible to have a chainable identity function that allows turbofish like that.

Mario Carneiro (Oct 20 2020 at 07:36, on Zulip):

Yeah, I suppose it is more appropriate for the grammar to use turbofish than put the type in parens, although now it's getting a bit verbose

Mario Carneiro (Oct 20 2020 at 07:36, on Zulip):

but especially if both options are available, it seems like a decent tradeoff: either use the verbose but chainable syntax or the one that requires surrounding parens

Jacob Lifshay (Oct 20 2020 at 07:37, on Zulip):

I was thinking of x.as<T> or x.as::<T>

Mario Carneiro (Oct 20 2020 at 07:38, on Zulip):

for options that use "as" in the name, I think we would have to somehow unify this with as casting, because otherwise the disconnect would be too confusing

Daniel Henry-Mantilla (Oct 20 2020 at 10:01, on Zulip):

@Josh Triplett for the case of into(), you can use the following helper trait:

trait Into_ : Sized {
    fn into_<U> (self: Self) -> U
    where
        Self : Into<U>
    {
        self.into()
    }
}
impl<T> Into_ for T {}

fn main ()
{
    let x = "Hello, World!".into_::<String>();
    dbg!(x);
}

I have made my own attempt to have an .as_::<...>() helper, but since it kind of leads to a chain of inference Rust is still confused about it:

trait As_ : Sized {
    fn as_<T : Is<EqTo = Self>> (self: Self) -> Self { self }
}
impl<T> As_ for T {}

"foo".into().as_::<String>() // still does not work

Back to the topic at hand, if inference / trait solving was made smart enough for As_ above to work, it would already be a big win that would render type ascription kind of "not that needed".

The one place where I would like and welcome type ascription is within patterns: that's where you can use type ascription to help make the code more readable (there is a reason IDEs already do this), and there even have been some suggestions that have built on top of it, such as enums with "anonymous variants" (not saying the suggestions have to necessarily be used, but it kind of shows the potential usefulness of ascription within patterns).

And that is a case where "adding its own binding" does not lead to cleaner code.

Daniel Henry-Mantilla (Oct 20 2020 at 10:16, on Zulip):

Addendum: another option, simpler than making As_ work, is what I call "copy the generic from trait position to method position", which, with our previous example, would have been bundling the into_ logic within the Into trait. The issue with that pattern at the moment, is that bounds such as U : Is<EqTo = T> lead to type inference errors even when T is know.
If that last issue was solved, then library authors with generics parameters in trait position would be able to offer the chance to users to specify what that generic param is with an added generic param added to the method that needs to be EqTo the outer one.

Not as pretty as a more general-purpose .as_, but may be simpler to implement, and in practice it could help a lot with most of these situations. But the lack-of-inference-leading-to-the-generic-param-needing-to-be-specified is a bummer in that regard

boats (Oct 20 2020 at 11:52, on Zulip):

scottmcm said:

I feel with let x: T existing it's hard to pick any other syntax.

That said, I've pondered making x as T be ascription in an edition change. We're getting closer and closer to having everything it can do have more targeted library methods instead (.cast() on pointers, transmute!, .into(), etc). So that would potentially open up that syntax instead.

My opinion is that type ascription (as an expression which takes an expression and a type) doesn't carry its wait, and we should instead consider more limited locations in which a user can insert a type (similar to the ability to insert a type in let bindings and turbofishes today). I don't think we should come up with an alternative syntax for type ascription as is.

nikomatsakis (Oct 20 2020 at 12:41, on Zulip):

Interesting thoughts. I too have some significant reservations about x: T syntax, and have long wanted it to be a more "method like" syntax (like foo.as<T>). I don't in general like "open-ended" types that don't have a closing delimiter (although we have that same problem with as). I do find I want it sometimes for calls to collect and other such cases.

Is there a canonical list of the places where type ascription is desired? This might be another good thing to add to the lang-team design notes. =) In particular, if we were going to try and pursue more limited places for annotation, as @boats suggested, it'd be useful to know what kinds of things people want to be able to annotate. The main one I know of is "method return type", which really seems to suggest to me that a general "ascribe type of this expression" is the best way to handle it. (I guess that another use case is triggering coercions to a dyn type?)

nikomatsakis (Oct 20 2020 at 12:43, on Zulip):

Based on this thread I wonder if removing type ascription syntax for now would make sense.

boats (Oct 20 2020 at 12:58, on Zulip):

I've most wanted it in match heads, things like match &foo: &str seem clearer and more obvious to me than match &foo[..] or match &*foo

boats (Oct 20 2020 at 12:58, on Zulip):

Though I'd also tbh like it even more if you could infer the deref coercion from the pattern type

kennytm (Oct 20 2020 at 15:23, on Zulip):

Suppose I want to write A::b(...).c(...).d(...).e(...), except I need ascription in one or two places before a call.

rfc#2522 introduced A::b(...).c(...):U.d(...).e(...):X, or with line breaks,

A::b(...)
    .c(...): U
    .d(...)
    .e(...): X;

( but 2522 was postponed because of the 5 concerns in https://github.com/rust-lang/rfcs/pull/2522#issuecomment-415551732 :shrug: )

Jake Goulding (Oct 20 2020 at 15:26, on Zulip):

given .await as something that has landed in the meantime

"method like" syntax (like foo.as<T>).

What prevents .as<T>? There's an amount of precedent for not-method method-like things now with .await after all.

kennytm (Oct 20 2020 at 15:29, on Zulip):

should it be a fish for consistency with normal generic function calls a.b::<T>().as::<U>() :upside_down:

Jake Goulding (Oct 20 2020 at 15:30, on Zulip):

I was also thinking that, but I know people keep trying to gut the fish.

kennytm (Oct 20 2020 at 15:33, on Zulip):

imo if the only difference between a.as<T> and a as T is operator precedence, it's better to fix the precedence than introducing a new syntax

(also, strong oppose if a.as<T> and a as T are both valid and have different semantics)

Mario Carneiro (Oct 20 2020 at 16:01, on Zulip):

If we can make as do type ascription (with or without the e.as<T> notation), then I think we don't really need e: T type ascription. And as has been in rust forever so it's much easier to train people to use it for type ascription as compared to introducing a new syntax. (If e.as<T> syntax is introduced, then like kennytm I think it must be a synonym for e as T.) But that might be tricky because if we don't actually know whether it's a type ascription or a coercion then the type inference problem becomes harder. Additionally, merging the two might cause a danger where something that was supposed to be a type ascription is in fact performing an as cast.

Jake Goulding (Oct 20 2020 at 16:44, on Zulip):

.become<T> :troll:

Jacob Lifshay (Oct 20 2020 at 17:33, on Zulip):

Type casting: x as T or maybe x.as<T> or x.as::<T>

Type ascription: x.type<T> or x.type::<T>

Jubilee (Oct 20 2020 at 18:11, on Zulip):

I like as doing casts, and not merely type assertions. I realize that both of these can technically be described as "type coercion", but one is "strong" and the other is "weak". I would find it much harder to understand why someone wrote as if it was both a strong and weak coercion. I would rather as become more capable of casting non-primitive types than it became a type coercion in some cases.

Josh Triplett (Oct 20 2020 at 19:22, on Zulip):

That's absolutely fair.

Josh Triplett (Oct 20 2020 at 19:23, on Zulip):

I do use as casts when I really do want to forcibly cast a numeric type. And I generally want type ascription for cases where I'd like to get an error if what I'm doing would have required an as.

scottmcm (Oct 20 2020 at 21:56, on Zulip):

Josh Triplett said:

I do use as casts when I really do want to forcibly cast a numeric type. And I generally want type ascription for cases where I'd like to get an error if what I'm doing would have required an as.

My general thought here is that "forcibly" (lossy) cast should be a method, just like the lossless From is. Because having syntax for the more-error-pront one but not for the information-preserving one seems backwards. Hence .wrapping_into() (https://github.com/rust-lang/rfcs/pull/2484) for every combination of integer types, for example.

Jubilee (Oct 21 2020 at 01:44, on Zulip):

One of the things noted in the portable SIMD group is that as makes casts very terse in math ops, which is very useful in many cases... in particular, the enum->integer casts that happen in a lot of embedded code is another place I've seen it used a lot. We were discussing it because of the tension between wanting to add e.g. an f32 to every lane in an f32x4 being explicit vs. being terse.

Jubilee (Oct 21 2020 at 18:55, on Zulip):

Maybe it would make more sense to use -> T. :upside_down:

Last update: Nov 25 2020 at 02:00UTC