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 ascription #803

Merged
merged 7 commits into from
Mar 16, 2015
Merged

Type ascription #803

merged 7 commits into from
Mar 16, 2015

Conversation

nrc
Copy link
Member

@nrc nrc commented Feb 3, 2015

Closes #354

rendered

@nrc nrc self-assigned this Feb 3, 2015
```
P ::= SP: T | SP
SP ::= var | 'box' SP | ...
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I think P vs. SP could be clarified here. pat: type is not a valid pattern (e.g., in match) today, and the only thing that resembles that syntax is let’s syntax. Is P supposed to represent what goes in between the let and = in let <P> = <value>;? If so, it should probably be clarified that P is not just a normal pattern.

@sanxiyn
Copy link
Member

sanxiyn commented Feb 3, 2015

RFC probably should say that this replaces currently limited type ascriptions in let and parameters(both fn and ||), and parameters in fn still must be typed patterns.


// With type ascription.
fn foo(Foo { a: i32, .. }) { ... }
```
Copy link
Member

Choose a reason for hiding this comment

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

What would we get inside foo:

fn foo(Foo { a: Bar, .. }: Foo<Bar>) { ... }
  • A variable named a, or
  • A variable named Bar (the current behavior)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. I don't think there is a good answer. I think the least worst is to assume that users will prefer the common pattern of using the same name for both the name of the field and the new variable (fn foo(Foo { a, .. }: ... in the current syntax), then assume that a single : in a pattern always denotes a type, i.e., we assume Bar is always a type (this is backwards incompatible, as you allude to). If a user wants to rename the variable, then they'd have to use a : b : _, which is bad. I think the alternative is theoretically nicer - type ascription should be more optional, but less practical, since it is common to reuse the field name.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say that it’s probably more common to want to rename the variable than to ascribe the type. After all, we don’t ascribe the type of any struct field patterns today (because we can’t), and that isn’t causing any major problems. The Foo { a, .. } notation is really just shorthand/syntactic sugar for Foo { a: a, .. }, so I feel it should have a lower priority than other more fundamental parts of the syntax. Foo { a: b: _, .. } (explicitly renaming) looks pretty bad and is not an obvious way of resolving the ambiguity, while Foo { a: a: Type } (explicitly type-ascribing) looks OK and is fairly obvious given that the shorthand is just optional sugar.

I think that Foo { a: b, .. } not working would be too surprising to be worth it, and the backwards-incompatibility also just makes matters worse. (Even better in my opinion would be to change struct initialisers to stop overloading :, but that’s already been discussed (in this RFC and elsewhere).)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, on second thoughts, this makes more sense. Avoiding the backwards incompatibility/code churn is especially desirable. I think I was over-estimating how often type ascription would be used.

Copy link
Contributor

Choose a reason for hiding this comment

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

While it is more common to want renaming than type ascription, I think Foo {x: x: Foo, y: y: Bar} is still a bit strange, would Foo {.x: Foo, .y: Bar} look better as a sugar? (If we don't change the value binding sigil to =>.)

@oli-obk
Copy link
Contributor

oli-obk commented Feb 3, 2015

Why not use an identifier and get around the similarity to struct initializers?
explicit conversion is "as". type ascription could be "is" (ok bad idea, typos would happen).
Maybe abuse "be", wouldn't require a new keyword, just sounds rather like bad english when reading?

@blaenk
Copy link
Contributor

blaenk commented Feb 3, 2015

I think type ascription is great, but I've never liked how everyone's always treated : as the only option for it. I personally find that using : makes everything harder to read. It's harder to pick out quickly and at a glance which parts of the code are type ascriptions, requiring one to scan the context. Personally I would prefer :: from Haskell, but everyone's hellbent on some half-baked attempt to make everything consistent and mirrored.

let z = if ... {
    foo.enumerate().collect() :: Vec<_>
} else {
    ...
};

Or anything else really.

@sfackler
Copy link
Member

sfackler commented Feb 3, 2015

@blaenk How should this be parsed?

let f = foo::bar::baz;

Is it the path foo::bar::baz, or the path foo::bar with the type ascription baz, or the ident foo with the type ascription bar::baz?

@jsanders
Copy link

jsanders commented Feb 3, 2015

@blaenk: Do you think you find name: type hard to pick out quickly because it is also used for other things, or just because it has very little "width" to it? I think your proposed syntax is pretty nice, but it would be extremely confusing if the syntax for type ascription were different than the annotations in let bindings and function parameters.

@blaenk
Copy link
Contributor

blaenk commented Feb 3, 2015

@sfackler fair enough, I completely forgot that paths also use ::. I still think : is difficult to read though :( I think what @oli-obk said are some good ideas, although I think be is reserved for a potential future tail call optimization 'ascription'? I think is would be nice and short, though he raises a good point about easily typod with as. However, I think as is not possible with every single thing (i.e. it'd raise an error to alert the programmer), and in instances when it could be either as or is, either one would be fine?

@jsanders Yeah, very little width is one thing, and also because it's used for explicit typing. When I look at code quickly, at a glance, I have to stop to look at the context to determine if it's an explicit type on a variable, function argument, etc., or if it's a type ascription. It becomes ambiguous to me whether something is a definition or a use of an already defined item. Note that I'm saying it's ambiguous to me, not the compiler.

Also I forgot to mention something that's mentioned in the drawbacks. If we ever --- and I think it's possible given previous discussions --- introduce named arguments, I would much rather have : be used there. The reason I don't think that my aforementioned ambiguity problem would arise there is because you'd have identifier: expression rather than identifier: type, where the latter is already used with explicit typing.

@jsanders
Copy link

jsanders commented Feb 4, 2015

@blaenk: Interesting, I guess I don't think much about the distinction between definition and use. Changing let x: u32 = 5; to let x = 5: u32 seems completely symmetric to me, but I suppose you could say that in the first case I've assigned the value 5 to a variable with explicit type u32 and in the second case, I've assigned the value of an expression with type u32 to a variable with implicit type. In what ways is that distinction important?

@blaenk
Copy link
Contributor

blaenk commented Feb 4, 2015

Yeah, I definitely don't think anyone would have much of a problem when it concerns literals, like your example shows.

However, my main concern was for example with function parameters, such as this one from the RFC:

foo(x: &[_], y: &[_]);

From a quick glance, because x and y aren't literals, it's easier IMO to confuse that with a function signature. Here's a possible alternative with is, though I'm not trying to push it in particular.

foo(x is &[_], y is &[_]);

let z = if ... {
    foo.enumerate().collect() is Vec<_>
} else {
    ...
};

Like I said I'm not trying to push is, perhaps there's a better future use for is than this. There's also always a group of people that say that it reads as if it should yield a boolean result, like a check, and may be confusing that way (I think that's just a matter of learning what is does in that case). It's just an example.

But anyways, this is pretty subjective. I'm pretty flexible and I'm sure that I would get used to it without complaining, I just think it's something to think about right now before we go forward.

Especially when we also consider if we would like : for named arguments, which I personally most definitely do. I think the combination of those two factors is enough to at least consider alternatives. If none are found, fair enough. I think the facility of type ascription is more important than the syntax, after all, and I do think we would greatly benefit from having it.

My point is that using an alternative syntax to avoid potential confusion and to allow the possibility of using it for named arguments is insignificant compared to actually having type ascription at all, especially given the other drawback: "Interacts poorly with struct initialisers."

@oli-obk
Copy link
Contributor

oli-obk commented Feb 4, 2015

since #601 (be -> become) is landing right now, "be" is free again and does not mean tail call anymore.
also, even become makes sense from an english language point of view, and i don't think it would ever collide with tail calls, since "become" would most likely be used like return, and not between an expression and a type

I agree with @blaenk that "is" sounds too much like something returning a boolean due to a runtime-check (or compile-time in case of a generic?)

@nikomatsakis
Copy link
Contributor

My current feeling:

👍 to the RFC as written. : feels like the right choice for the reasons listed below, and having the precedence match as seems right. Patterns are perhaps a little trickier, but the precedence as written seems right: &x: T, &mut x: T, box x: T. In all those "unary operator" cases, it seems like there are parts of the type already basically implied by the operator, and it's not clear why one would want to repeat those (box patterns are a semi-exception; but they are feature-gated, right?)

👎 to ::, is and become as alternatives. I agree they add a degree of clarity, but : allows us to generalize the existing annotations on patterns in a natural way, and that seems very worthwhile. Also, there is a lot of precedent from other languages. I don't find confusion because a fn def'n and a fn call very likely, given that fn def'ns involve the fn keyword.

@quantheory
Copy link
Contributor

I agree with @nikomatsakis, only more so. I think that : is actually clearer than any of the keywords proposed here, simply because of its use in type declarations in Rust already, and because it is sometimes used for definitions (e.g. in glossaries).

x be Foo and x become Foo are both just puzzling at first glance. Using "become" sounds like a cast because it implies a change. Using "be" sounds like a cast because in English it's used for the imperative mood (e.g. dave be Careful looks like the English sentence "Dave, be careful!"). x is Foo might be a little more inherently clear than x: Foo, but it's less consistent with Rust itself, and more confusing for people coming from other languages, like Python, where the is operator tests for association.

(I also don't actually like the idea of using : for named/keyword arguments, so to me that's not a good reason to pick something else. In fact I don't much like that it is used for struct initialization either. I rather wish that we had used =, <-, =>, or some other symbol that implies "association" or "assignment" rather than "definition", though two of those suggestions would technically be ambiguous in the keyword argument context right now.)

@brson
Copy link
Contributor

brson commented Feb 5, 2015

I feel like this is something that we do not have to do now or any time soon - it's a nice-to-have.

@1fish2
Copy link

1fish2 commented Feb 5, 2015

I don't think this is a bad feature, but making expressions more complicated has more costs than benefits. (In patterns, the proposal removes some complexity.)

How many let declarations in the library are we itching to combine into longer expressions? Why is that compelling? The Scala Style Guide says:

Ascription is basically just an up-cast performed at compile-time for the sake of the type checker. Its use is not common, but it does happen on occasion.

Benefits

  • foo.enumerate().collect() : Vec<_> is more concise than
    let c: Vec<_> = foo.enumerate().collect(); c

That may be more convenient but overall usability can suffer by making expressions stranger and by encouraging longer expressions.

Sometimes you can just write a literal suffix like 5u32 or expression as type.

Costs

  • Harder to learn the language. One more interpretation of : and one more feature in general.
  • Larger language spec, tutorials, books, etc.
  • Adds complexity to every compiler, language test suite, refactoring editor, program analysis tool, syntax-directed editor, and syntax-coloring web site (like GitHub and Discuss).
  • Opportunity costs for all of these.
  • Interferes with key: value syntax for default function arguments and struct initialization syntax.

@huonw
Copy link
Member

huonw commented Feb 5, 2015

I'm in favour. I have a library where being able to write:

let foo =
    something.bitcast() : &[u8x16]
        .convert() : &[u8]
        .some_method();

would be useful.

The methods are defined like trait Bitcast<T> { fn bitcast(self) -> T; }, meaning methods themselves aren't generic, so e.g. .bitcast::<&[u8x16]>() doesn't work, but the type annotations are required, since the T type is uninferrable. Being required to separate out those expressions into temporaries is very annoying; it makes sense sometimes, but is annoying at others.

(I imagine this may apply to a generic From trait which has been occasionally discussed, too, and anything with multidispatch.)

On that note, my example is assuming that type ascription works inside method chains like that, which requires having . never being part of a type?

@quantheory
Copy link
Contributor

@1fish2: I don't think that this is particularly worse than (or even very different from) the situation with as casts from a parser's perspective. I can probably think of a half dozen somewhat-recent features that are likely to be more troublesome either for users or tools that parse syntax (off the top of my head, UFCS and the .. range literals). That doesn't mean that your point isn't valid, but I feel like this feature is one of the most innocent recent RFCs, as far as adding language complexity is concerned.

My experience with statically typed languages with inference (particularly Haskell is relevant here) is that, even for applications that don't use much type ascription directly, there is a benefit to type ascription for debugging/pedagogy. If you have a type error in code that's using fairly generic functions, but you're not entirely familiar with how the types in a given API work together, you can sometimes use type ascription to figure out which function in the chain is returning something different from what you expect.

That is, you can very quickly iterate with type ascription, in any arbitrary spots in a complex expression, to trigger a type error that's better than the one the compiler gave you at first, and/or to discover which step prevented type inference from resolving an ambiguity. Then you can easily yank the ascription back out of the code if you have a better fix, and it's not needed anymore.

I also think that using type ascription improves readability significantly compared to the alternatives. To be frank, I would much rather have type ascription up front, with some of the weird details of UFCS syntax being the arcane you-should-learn-this-last feature, than to have no type ascription and rely on extra let statements and UFCS for these things. Adding a let is "just" adding verbose boilerplate most of the time (potentially cluttering code with boilerplate declarations of single-use variables that have names like "temp" and "x"), but it can potentially cause some confusion of its own if you accidentally change a lifetime.

Type ascription is one of the simpler features to learn, and I would expect it to be something mentioned pretty early on in most tutorials and books. IMO, the existing workarounds have a much higher cost both for learners and for developers using Rust day-to-day.

@huonw: I like that example. If it doesn't work, there's always this:

let foo =
    ((something.bitcast() : &[u8x16])
        .convert() : &[u8])
        .some_method();

Not so elegant, but possibly less annoying than adding two extra let statements in these cases...

@1fish2
Copy link

1fish2 commented Feb 5, 2015

Thanks, @quantheory ! That's informative, and those techniques ought to be in tutorials.

Agreed, it's the volume of features that concerns me, not really this particular one.

@eddyb
Copy link
Member

eddyb commented Feb 5, 2015

The alternatives mentioned (literal suffixes and as casts) are actually problematic - type ascription would only trigger coercions and type assignments that naturally happen already, while casts are used for potentially overflowing (or truncating, right now) integer conversions, pointer-to-integer/integer-to-pointer and *const T to *mut T.

@brson If we do this now maybe we have a chance to remove literal suffixes from the language? At the very least, using as where coercions work shouldn't be possible, to reduce the meaning of the operator to "potentially dangerous" casts.

Maybe that's undesired, but I am feeling very confident about cleaning up those bits of syntax and semantics.

@oli-obk
Copy link
Contributor

oli-obk commented Feb 5, 2015

+1 to using type ascription instead of literal suffixes.

@iopq
Copy link
Contributor

iopq commented Feb 5, 2015

I'd rather have type ascription than literal suffixes, and I would rather change struct notation than lose type ascription.

I didn't care about this feature until I had to work with traits... when writing tests especially you need to have the exact type of basically everything. I had a None in my test that I knew was really a None: Option<String>, but this wasn't legal outside of a let statement. I had to ask on IRC to find out I'd have to write None::<String> which is syntax I'm not familiar with.

Working with closures is another example where I would like to just say

let tuples = [
    ("Fizz", (&|i: i32| i % 3 == 0) : &Fn(_) -> _ ),
    ("Buzz", &|i: i32| i % 5 == 0),
];

but I have to write THIS:

let tuples: [(_, &Fn(_) -> _); 2] = [
    ("Fizz", &|i: i32| i % 3 == 0),
    ("Buzz", &|i: i32| i % 5 == 0),
];

I can't omit even the length of the array, so when I add more stuff to it I have to manually fix it:

let tuples: [(_, &Fn(_) -> _); 3] = [
    ("Fizz", &|i: i32| i % 3 == 0),
    ("Buzz", &|i: i32| i % 5 == 0),
    ("Bazz", &|i: i32| i % 7 == 0),
];

Type ascription is a huge ergonomics deal in current Rust until type inference is much better than what it is now

@glaebhoerl
Copy link
Contributor

I'd rather have type ascription than literal suffixes, and I would rather change struct notation than lose type ascription.

Ditto to both. (I'd also rather change struct notation than not change struct notation, but type ascription becoming collateral damage would add injury to insult.)

@quantheory
Copy link
Contributor

Do consider the ticking clock for 1.0. If we change the struct syntax now, it's backwards compatible to add type ascription post-1.0. (We could also not change struct initializers at all, at the cost of having to cut out any parts of this RFC that would cause an ambiguity.) If we remove literal suffixes, however, we would want type ascription immediately in order to replace them, which means that it would have to be implemented ASAP.

Once you have type inference and type ascription for unsuffixed literals, literal suffixes are saving very little ergonomically. So I agree that in an ideal world we could just remove them as a redundant language feature. But I think it's up to the core team whether they think that they would be able to implement this RFC quickly enough to break backwards compatibility here.

@eddyb
Copy link
Member

eddyb commented Feb 5, 2015

@quantheory this RFC exists because I implemented type ascription on a dare :). Only for expressions, but that seems to be what you're talking about. It would be tinier if it weren't for that weird asm! syntax borrowed from GCC.

@nikomatsakis
Copy link
Contributor

Tracking issue rust-lang/rust#23416

@nikomatsakis nikomatsakis merged commit e215e59 into rust-lang:master Mar 16, 2015
@nikomatsakis
Copy link
Contributor

nrc added a commit to nrc/rfcs that referenced this pull request Mar 18, 2015
nrc added a commit to nrc/rfcs that referenced this pull request Mar 23, 2015
@strega-nil
Copy link

@nikomatsakis Question: How does this apply to match statements? I believe this would be a very useful case when matching on * pointers, and would completely invalidate #1009, which would be nice.

@nrc
Copy link
Member Author

nrc commented Mar 24, 2015

@GBGamer what you want (I believe) is type ascription in patterns (a pattern is the general version of the thing to the left of => in a match arm). That was originally part of this RFC, but I took it out to make landing quicker. I still hope to add type ascriptions in patterns at a later date, and that is tracked by #354.

@strega-nil
Copy link

Cool, thanks :)

On Tue, Mar 24, 2015 at 3:28 PM, Nick Cameron notifications@github.com
wrote:

@GBGamer https://github.com/GBGamer what you want (I believe) is type
ascription in patterns (a pattern is the general version of the thing to
the left of => in a match arm). That was originally part of this RFC, but
I took it out to make landing quicker. I still hope to add type ascriptions
in patterns at a later date, and that is tracked by #354
#354.


Reply to this email directly or view it on GitHub
#803 (comment).

"Unjust laws exist; shall we be content to obey them, or shall we endeavor
to amend them, and obey them until we have succeeded, or shall we
transgress them at once?"
-- Henry David Thoreau

@ticki
Copy link
Contributor

ticki commented Nov 20, 2015

What's the state of this?

@nagisa
Copy link
Member

nagisa commented Nov 20, 2015

No implementation.
On Nov 20, 2015 12:12 PM, "Ticki" notifications@github.com wrote:

What's the state of this?


Reply to this email directly or view it on GitHub
#803 (comment).

@petrochenkov
Copy link
Contributor

Type ascription seems to be cursed - everyone going to rebase rust-lang/rust#21836 disappears immediately.

@vitiral
Copy link

vitiral commented Aug 17, 2016

I'm not sure what the process is supposed to be for this, but the syntax chosen for this has conflicted with other desireable language features, aka https://internals.rust-lang.org/t/pre-rfc-named-arguments/3831/103

Not necessarily that we should implement that feature, but the fact that it conflicts gives credence to how the syntax migh be confusing.

I'm also having difficulty seeing how the (simple) example given in the RFC:

// With type ascription.
let z = if ... {
    foo.enumerate().collect(): Vec<_>
} else {
    ...
};

could not be accomplished with turbofish

// With turbofish
let z = if ... {
    foo.enumerate().collect::Vec<_>()
} else {
    ...
};

Not that turbofish is the greatest syntax ever, but it is in std and serves a purpose. Isn't turbosh nearly orthogonal to this feature, and if so shouldn't this feature be removed?

I am worried about additional syntax appearing in the std language, making things even harder to parse for developers.

@petrochenkov
Copy link
Contributor

petrochenkov commented Aug 17, 2016

@vitiral
The whole point of the type ascription syntax is symmetry between declarations and expressions:

let a: u8 = 10;
let a = 10: u8;

Changing it would put one more nail in type ascription's coffin.
The ambiguity between type ascription and named arguments can be resolved in favor of named arguments with minimal forward lookup and no breakage on stable.

I'm also having difficulty seeing how the (simple) example given in the RFC ... could not be accomplished with turbofish

There are cases when type hint can't be provided through generic parameters/arguments, e.g.

a.into(): B

Into::into doesn't have generic parameters so you can't write a.into::<B>().

@strega-nil
Copy link

strega-nil commented Aug 17, 2016

I'm not sure about "coffin"... type ascription doesn't really have a coffin. It's a really great feature, and we're not likely to try and support a special-cased and specialized feature (named parameters) over a very-much wanted and very useful feature (type ascription).

@jsgf
Copy link

jsgf commented Jun 24, 2017

What's the state of this feature?

It seems to me that this would be very useful for futures-oriented programming. Because expressions of composed futures/streams get very complex types, it would be very useful to be able to "pin" parts of an expression to expected types, both to help type inference and to get better error messages. Current mechanisms to do this (let bindings and type-assertion functions) are cumbersome by comparison.

(ping @alexcrichton, @brson as it came up in conversation with them a couple of months ago)

@petrochenkov
Copy link
Contributor

@jsgf
[meta] The tracking issue is rust-lang/rust#23416, it's better to consolidate the discussion there.

@Centril Centril added A-syntax Syntax related proposals & ideas A-expressions Term language related proposals & ideas A-ascription Type ascription related proposals & ideas A-typesystem Type system related proposals & ideas A-coercions Proposals relating to coercions. labels Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ascription Type ascription related proposals & ideas A-coercions Proposals relating to coercions. A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Type ascription (ascription of patterns)