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 (ascription of patterns) #354

Closed
nrc opened this issue Oct 6, 2014 · 41 comments · Fixed by #803
Closed

Type ascription (ascription of patterns) #354

nrc opened this issue Oct 6, 2014 · 41 comments · Fixed by #803
Labels
postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@nrc
Copy link
Member

nrc commented Oct 6, 2014

Post-1.0, we would like to allow arbitrary type ascription - that is annotating any expression with a type. E.g., let _ = foo(x, y: Bar<int>, z); (type ascription on the sub-expression y).

Detail to be nailed down - precedence (probably the same as as).

Optional extra - type ascription on patterns, e.g. let (x: Bar<int>, y) = foo(...); - useful when you care about the type of some part of the pattern but not others, especially when the bits you don't care about are _.

Optional, optional extra - using the ascribed types in pattern matching for downcasting (an extension of some #349 proposals).

@nrc nrc added the postponed RFCs that have been postponed and may be revisited at a later time. label Oct 6, 2014
@brendanzab
Copy link
Member

👍

Generalizing the (:) operator would be nice. How does it interact with the struct field matching syntax though?

struct Foo<T> { a: i32, b: T }
let foo = Foo { a: 1, b: 2.0 };
let Foo { a: x, b: y: f32 } = foo;

@huonw
Copy link
Member

huonw commented Oct 6, 2014

@bjz the identifiers in the shorthand let Foo { a, b } = foo; are not full patterns, so it's not necessarily crazy for us to just require the form you have written (i.e. let Foo { a: i32, .. } is binding the value of the field a to the identifier i32, not type ascription). This should definitely be discussed in a detailed description of this feature, though.

@nrc
Copy link
Member Author

nrc commented Nov 2, 2014

See also some discussion on rust-lang/rust#10502

@1fish2
Copy link

1fish2 commented Feb 1, 2015

What's the compelling reason for these features?

I can see how type ascription could enable safer patterns, but in expressions it seems like excess syntax sugar and complexity.

Benefits

  • expression: type is more concise than let v: type = expression; v

The alternative is to write an ordinary let statement or (in some cases) expression as type or a literal with suffix like 0i16.

Costs

  • Makes learning the language harder.
  • May confuse people when reading unfamiliar code.
  • Adds complexity to the language spec, tutorials, books, API docs, sample code, and published articles.
  • Adds complexity to every compiler, language test suite, refactoring editor, program analysis tool, syntax-directed editor, and syntax-coloring web site (like GitHub).
  • Prevents using the usual key: value syntax for default function arguments.
  • May interact with other future syntax.
  • Enables scary looking code like this:
SomeStruct {
  key1: value1: type1,
  y: y: Bar<int64>
}

Rust aims to be a production language that's predictable and reliable for security-critical and safety-critical software. Be careful about adding features from research languages like Haskell and Scala.

@brendanzab
Copy link
Member

Be careful about adding features from research languages like Haskell and Scala.

Rust has tons of features from 'research languages', and is all the better for it, especially when it comes to safety and security.

@1fish2
Copy link

1fish2 commented Feb 1, 2015

Indeed. Borrow the ideas that have proven out.

@japaric
Copy link
Member

japaric commented Feb 1, 2015

There is an implementation in rust-lang/rust#21836

@ghost
Copy link

ghost commented Feb 1, 2015

Indeed. Borrow the ideas that have proven out.

Type annotation/ascription is one of the oldest ideas in programming languages.

@1fish2
Copy link

1fish2 commented Feb 1, 2015

@darinmorrison for anonymous expressions? And proven worthwhile? Do say more.

Google mostly turns up info like the Scala Style Guide:

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.

Criteria for language features should include usability, high bang/buck for the ecosystem, and not interfering with future plans. Apparently, type ascription for expressions is rarely used in Scala and accomplishes no more than using two orthogonal features together.

Optional extra - type ascription on patterns", e.g. let (x: Bar<int>, y) = foo(...);

That seems to be a declaration for a let binding rather than ascription.

@codyps
Copy link

codyps commented Feb 1, 2015

@1fish2 I asked about something like this on IRC several months ago. The solution suggested to me was to add a macro to accomplish typing expressions. If we don't have support for it in the language, people will add a macro for it to individual projects, and all your arguments about confusion still apply or are made worse (naming and syntax could vary across codebases.

An obvious alternative would be a standard macro that supplies the functionality, but I'm not sure if that's really a better choice than adjusting the syntax.

Also, I'd be careful to note that your example with a struct is not necessarily something we expect (unless I'm a fool and those internal struct members are somehow expressions). As @huonw mentioned, we'll need to figure out if that makes sense (as a separate concern from being able to apply : to expressions)

@ghost
Copy link

ghost commented Feb 1, 2015

@1fish2 Yes. The idea is about as old as types themselves. It may not have been common in some of the most popular languages but that doesn't have any bearing on its utility.

One strong argument in favor of allowing inline annotations is that it opens up options for making the type system more flexible, and potentially simplifying it even. If Rust ever adopts a bidirectional typing algorithm internally (I don't know what it currently uses), having these annotations will likely be important. (Some detail here and here). Top-level annotations are not always natural to use and may not even be feasible in some cases.

Advocating for keeping the language simple and avoiding unnecessary features is admirable but I don't see this one as being a problem, at least not in the sense you have suggested so far.

@1fish2
Copy link

1fish2 commented Feb 1, 2015

+1. Go for a standard macro. That avoids the problems and costs.

Programmers can handle a great deal of complexity so it takes much restraint to avoid adding little features one by one that build up enormous complexity and sharp edges, like C++. Usability death by a thousand additions.

@eddyb
Copy link
Member

eddyb commented Feb 1, 2015

I have seen enough people being confused about 1i not being a complex number and 500us not being half a millisecond - 1: isize and 500: usize would have never created that confusion to begin with (this is one reason I'd like to see integer suffixes completely removed, but I digress).

I have a hard time finding PL features that aren't as equally represented (or more so) than type ascription, for most of your "Costs" bullet points, aside from the last 3 which are a single point.

I am actually curious what languages use x: y and which went for x = y, out of those that support named function arguments (and what other flavors there are, of course).

@dgrunwald
Copy link
Contributor

-1. Using : for type ascription is confusing given that it already has another meaning in struct literals.
Also, I'd like to eventually see : used for named arguments.

I feel like the Rust syntactic space is already becoming over-crowded, and it will be difficult to find room for future language extensions. Please avoid adding new symbolic operators unless absolutely necessary.

@eddyb
Copy link
Member

eddyb commented Feb 1, 2015

And no, macros are not a solution. You cannot create a let binding without a block, which introduces a lifetime scope. See rust-lang/rust#21354 as an example, which was solved with fully qualified UFCS:

// Original implementation (bad for inference and lifetime scopes):
{
    let xs: Box<[_]> = Box::new([$($x),*]);
    slice::SliceExt::into_vec(xs)
}
// New implementation:
<[_] as slice::SliceExt>::into_vec(Box::new([$($x),*]))
// Slightly better if we ignore the temporary Box::new usage:
<[_] as slice::SliceExt>::into_vec(box [$($x),*])
// And type ascription:
(box [$($x),*]: Box<[_]>).into_vec()

Now you tell me what's more complicated and damaging.

Another data point: if we decide to replace pat: type syntax with patterns which may include type ascription patterns (not implemented in my PR), we could replace something like this:

pub fn noop_fold_where_clause<T: Folder>(
                              WhereClause {id, predicates}: WhereClause,
                              fld: &mut T)
                              -> WhereClause {...}
// With this nicer version:
pub fn noop_fold_where_clause<T: Folder>(WhereClause {id, predicates}, fld: &mut T)
                                         -> WhereClause {...}

Now that I think of it, that may not be the best example - fn foo(Point {x, y}) may be more appealing - but it is one I've had to deal with in the past.

@codyps
Copy link

codyps commented Feb 1, 2015

@dgrunwald you bring up a good point: using : in struct literals has an entirely different meaning from from : everywhere else: "has value" vs "has type".

If we could go back in time, it might make sense to avoid that mixed meaning and use (say) = for "has value" within struct literals.

Off topic, but: Going forward, I don't think introducing more users of the "has value" meaning for : is the best idea. Is there any reason = is unusable for named args?

@eddyb
Copy link
Member

eddyb commented Feb 1, 2015

@jmesmon I believe foo(x = 0) being named arguments and foo((x = 0)) being equivalent to x = 0; foo(()) is doable, just need to convince everyone it's a good idea.

As for the current use in struct literals, I've got mixed feelings:

// Out of these two, I find the JS(ON)-like syntax most pleasing:
Point { x: x, y: y }
Point { x = x, y = y }
// But here, using equal signs conveys the intention better:
Point { x: x: f32, y: y: f32 }
Point { x = x: f32, y = y: f32 }

@1fish2
Copy link

1fish2 commented Feb 1, 2015

There's a substantial forum discussion about struct initializer syntax where that's on topic.

@dgrunwald
Copy link
Contributor

@eddyb: Using foo(x = 0) for named arguments would be a breaking change. We'd have to restrict the usage of = before 1.0 to keep that option available.

@huonw
Copy link
Member

huonw commented Feb 2, 2015

And no, macros are not a solution. You cannot create a let binding without a block, which introduces a lifetime scope. See rust-lang/rust#21354 as an example, which was solved with fully qualified UFCS:

I think using match should work:

match $input { a => { let x: $type = a; x } }

Although this may not actually give the same behaviour.

@eddyb
Copy link
Member

eddyb commented Feb 2, 2015

@huonw that's a nice way of solving the lifetime issue, but it's still useless for coercions.

@glaebhoerl
Copy link
Contributor

Oh geez. Type ascription has been in the plans since near-forever, only put off because it's backwards compatible, non-critical and can be added after 1.0. I've always been bothered by others not being bothered by the obvious syntactic conflict with it that : in struct literals presents. Surely, I thought, if we already had type ascription, and Point { x: y: i8 } were up in peoples' faces, then there would be greater urgency around changing struct literals to use =? And now @dgrunwald is arguing the reverse direction, that this bit of ugliness should block adding type ascription. And, I mean, he might have a point. But this is a silly state of affairs.

(For what it's worth (which, post-alpha, is zilch), my preferred syntax for struct literals (and likewise patterns) would actually be Point { .x = 10, .y = 11 }, which also mirrors field access. Sorry for going off topic.)

@eddyb
Copy link
Member

eddyb commented Feb 2, 2015

I'd never seen that syntax before in the context of Rust and I kinda like it, I wish we had this discussion before the alpha (is it really too late for miracles?).
It would fit well in with a likely expansion of move semantics:

Point { .x = 10, .y = 11 }
// would expand to the more verbose
{ let p: Point; p.x = 10; p.y = 11; p }
// even if that would be safe, I doubt it will be overused as the
// struct literal syntax is always shorter - except if this worked:
{ let p: Point; (p.x, p.y) = (10, 11); p }
// but why not
Point { (.x, .y) = (10, 11) }

We can still change this... right?

@eddyb
Copy link
Member

eddyb commented Feb 2, 2015

Which leads to a further extension - if we had IndexSet:

HashMap { ["foo"] = bar }
// as sugar for:
{ let m = HashMap::new(); m["foo"] = bar; m }

@bluss
Copy link
Member

bluss commented Feb 2, 2015

If you bring up named arguments, let's unify struct constructors with that.

struct Point { x: i32, y: i32 }
let p = Point(x = 1, y = 2);

@glaebhoerl
Copy link
Contributor

We can still change this... right?

I wish so, but since the alpha I've seen three reasonable syntax change RFCs (full disclosure: two were my own) summarily closed. Have any been accepted?

{ let p: Point; (p.x, p.y) = (10, 11); p }

This is #372 :)

HashMap { ["foo"] = bar }

Yeah, C# 6 added this as well. That's actually where I got the idea for Point { .x = ... from in a roundabout way (rather than someplace more obvious, like C99).

@eddyb
Copy link
Member

eddyb commented Feb 2, 2015

#372 would be a pain to analyze or trans, otherwise we would have it already. There's no real grammar issues there AFAICT, just wish we had a proper MIR...

@1fish2
Copy link

1fish2 commented Feb 2, 2015

Nobody has explained why type ascription on expressions is desirable for Rust. How many let statements in the library could be folded into expressions and why would that be worthwhile? Feature envy for Scala?

Language design should design for usability and do usability testing. It's easy for one person to add a feature and know what it does; not easy for all future programmers to come across yet another feature in code or docs and find out what it does and how it interacts with other features.

Guido van Rossum on "Language Design Is Not Just Solving Puzzles":

Mathematicians don't mind these [Rube Goldberg contraptions] -- a proof is a proof is a proof, no matter whether it contains 2 or 2000 steps, or requires an infinite-dimensional space to prove something about integers. Sometimes, the software equivalent is acceptable as well, based on the theory that the end justifies the means. ... And there's the rub: there's no way to make a Rube Goldberg language feature appear simple. Features of a programming language, whether syntactic or semantic, are all part of the language's user interface. And a user interface can handle only so much complexity or it becomes unusable.

@ghost
Copy link

ghost commented Feb 2, 2015

Nobody has explained why type ascription on expressions is desirable for Rust.

I think they have. But here it is in a nutshell: Rust doesn't have global decidable type-inference. That means that sometimes annotations or hints are necessary. Without having inline annotations, you have to use something more elaborate like these macros with let or match, which may not always work due to language semantics (@eddyb pointed out one instance of this), or may change the meaning of your program in subtle unintended ways. Annotation should be a no-op (erased to the underlying term).

There's plenty of other examples of why they are useful both in theory and practice. Adding type annotations to the language would hardly turn Rust into Scala or Haskell. From my perspective, if you really believe that this would be a harmful feature, the burden should be on you to justify your position with something concrete.

@1fish2
Copy link

1fish2 commented Feb 2, 2015

I don't actually think this proposal is terrible but I don't see the gain. Can you explain it an engineer? Please do give those examples. What concerns me is usability and learnability are under duress from a wave of feature proposals (and we need a production language that can replace C/C++ for safety-critical and security-critical software).

I'm sorry, brother @darinmorrison but I tried to read your paper and had no idea what the math notation meant or how it relates to this proposal. Does "global decidable type-inference" mean a type probability cloud collapses over the whole program, yielding an eigen-type? It seems to me that limited, local inference would be more predictable and reliable.

(Is there a doc on Rust type inference?)

may change the meaning of your program in subtle unintended ways

How would adding a let (dropping the macro idea) do that?

From my perspective, if you really believe that this would be a harmful feature, the burden should be on you to justify that position with something concrete.

I think I have. The proposal adds complexity and associated costs. It interferes with syntax for struct init and (future) default arguments. We could also do a quick usability test by showing examples to programmers and asking them to interpret.

@ghost
Copy link

ghost commented Feb 2, 2015

Does "global decidable type-inference" mean a type probability cloud collapses over the whole program, yielding an eigen-type?

If type inference is decidable, it just means that for any program P, the compiler can infer (produce) a type A such that P has type A (i.e., P: A) or otherwise deduce that no such A can possibly exist, in which case your program P has a type-error somewhere.

If type inference were global, it would try to do this across all parts of the program, even without top-level annotations on functions. This is like what the ML folks do but not how Rust works.

It seems to me that limited, local inference would be more predictable and reliable.

Well, yes. And Rust does not try to do global inference as far as I understand. But even if you restrict to some sort of local inference, it still does not mean you can avoid annotations or hints entirely.

@glaebhoerl
Copy link
Contributor

@1fish2 Rust currently allows specifying the type of a thing using : in a few specific places, like function arguments and lets. "Type ascription" is really just generalizing this existing syntax to apply to any expression (or pattern). Think of a let without a specified type. Now specify the type. Was this pernicious? Adding a type ascription to a given expression would implicate the same amount of perniciousness. Type inference is a mechanism for propagating type information from one part of the program, where it has been provided, to others, where it hasn't, to avoid the programmer having to repeat herself unnecessarily. Type ascription merely provides greater flexibility in where the inputs to this process can be provided. It is just about the most benign language feature I can possibly imagine. (As @darinmorrison writes, it is literally a no-op, and only there to help the typechecker figure out what you meant.) From a practical perspective, as @eddyb also notes, it could supplant current uses of the (rather ugly) explicit "UFCS" and type instantiation syntax. Instead of transmute::<From, To>(thing), you could write transmute(thing): To. (Only using transmute as an example because I can't think of a better generic function offhand.)

I don't know Scala, and have no idea what connotations the feature might carry there. The syntactic conflict with struct literals is deeply unfortunate, as I already wrote.

@1fish2
Copy link

1fish2 commented Feb 2, 2015

Thanks. Here's a Scala example of type ascription:

import scala.collection.mutable
var ss = Set("Hello")
var ps = Set("Hello": Object)

now ps += Nil will compile while ss += Nil won't because ss is a Set[String]. Without type ascription, we'd have to write

val hi: Object = "Hello"
var ps = Set(hi)

or

var ps: Set[Object] = Set(hi)

or

var ps = Set[Object]("Hello")

which is clearer and just as concise.

Anyway, for the transmute example this proposal would simply let you write

... transmute(thing) : To ...

instead of

let x: To = transmute(thing);
... x ...

I don't think type ascription is a terrible thing but why pay all those costs to make a rare case more compact?

The part that worries me is the drive to add features.

nrc added a commit to nrc/rfcs that referenced this issue Feb 3, 2015
@nrc nrc mentioned this issue Feb 3, 2015
@nrc
Copy link
Member Author

nrc commented Feb 3, 2015

See #803

@nrc
Copy link
Member Author

nrc commented Mar 16, 2015

Reopening since #803 does not cover pattern ascription.

And edited the issue title to reflect that.

@nrc nrc reopened this Mar 16, 2015
@nrc nrc changed the title Type ascription Type ascription (ascription of patterns) Mar 16, 2015
@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 15, 2015
@mikeyhew
Copy link

mikeyhew commented Nov 2, 2016

Some people have commented that they would like to see more examples of why type ascription would be useful. I can't speak for all cases - in particular I don't really get foo(x: i32) when you can already do foo(x as i32) - but in the case of patterns, they really would be useful, even for newer, less sophisticated users of Rust. Take this example of me learning how to use diesel:

fn main() {
    let connection = PgConnection::establish(DB_URL).expect(&format!("Error connecting to {}", DB_URL));

    use schema::companies::dsl::*;
    use models::*;
    let results = companies.load::<Company>(&connection).expect("error loading models");

    for company in results {
        // what is the type of company?
    }
}

I think that results is an iterator of Company structs. But I don't know, it could be an iterator of Option<Company> or something else. What I'd like to do is a sanity check: annotate the type of company, and run the compiler and see if I get a type error.

for company: Company in results {

}

Unfortunately, that's not allowed at the moment.

To a newcomer to Rust, the above seems like something you should be able to do. Why? Because you can do it for normal let statements:

// this compiles fine
let company: Company = something_that_has_type_company;

So why can't you do it in a for ... in loop? A similar argument goes for if let and while let statements: shouldn't you be able to do this?

if let Some(company: Company) = results.into_iter().next()

In the nature of making the language more consistent, and to allow these types of sanity checks, we should allow type annotations like this on all the let statements - I believe they are called patterns - as well as for ... in statements, which desugar to a while let as far as I understand.

@steveklabnik
Copy link
Member

annotate the type of company, and run the compiler and see if I get a type error.

Most people do something like

let () = company;

which will error as long as company's type is not ().

@mikeyhew
Copy link

mikeyhew commented Nov 4, 2016

@steveklabnik You're right, you can do that, and for the above example I could have done something similar:

for company in results {
    {
        let c: &Company = &company;
    }
   // ...
}

But why can't you just put the type you're expecting into the for loop? Again, it should be consistent.

@nrc
Copy link
Member Author

nrc commented May 29, 2017

Another example: let (defs, refs): (Vec<_>, Vec<_>) = sigs.into_iter().map(|s| (s.defs, s.refs)).unzip();, I'd rather write let (defs: Vec<_>, refs: Vec<_>) = sigs.into_iter().map(|s| (s.defs, s.refs)).unzip();.

@Centril
Copy link
Contributor

Centril commented Oct 7, 2018

Closing in favor of #2522.

@jtmoon79
Copy link

jtmoon79 commented Aug 10, 2022

Consider this comment deleted. Updated comment at #2522 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging a pull request may close this issue.