Raising the bar for introducing new syntax

No, it does not- that is due to orthogonal choices. There's no need to conflate Go's lack of generics with its simplicity, especially when the designers are at this moment working on adding them in some form.

Let's take another example, since I said there were a lot of languages with this goal. To cater more toward your personal preferences, let's look at 1ML. Their goal is to unify previous MLs' features into a "small and consistent language." 1ML has generics and sum types and modules, so clearly this goal is not incompatible with your pet features.

What it is incompatible with is something like Haskell, which is in a lot of ways the C++ of the ML family. And that's fine, because Haskell's goals involve being a place where people can research new language features! This obviously isn't a perfect comparison, but Haskell is a huge language with quite a few overlapping "feature zoo" situations.

I will also say, as has been said before, that things like field initialization shorthand, GATs, try blocks and try fn, and const fn are not "feature zoo" candidates, specifically because they "fill in" a very particular kind of "holes" in the language, removing special cases and making Rust more orthogonal.

This is in stark contrast to impl Trait or throw, which introduce new special cases and make the language less orthogonal. For example, in a perfect world without implementation time constraints, I would have preferred that we just got abstract type without impl Trait, and especially not impl Trait-in-argument-position.

3 Likes

Being able to initialize type Foo struct {Baz, Bar} with Foo {bar, baz} is a horrible footgun in Go (did you notice the bug?), and names repeated across lines are a documented source of bugs.

In Go you don’t have to learn about Result/Option/? and the rest of the error “feature zoo”, but you pay for that with endless if err != nil and creeping nil dereferences at run time.

I think Rust is doing great by evaluating each bit of syntax whether it provides more value in usability than it costs in learning.

9 Likes

I explicitly exclude Result from the term “feature zoo,” so maybe we can stop beating up that particular Go punching bag.

2 Likes

Interesting - I wonder how consistent it will end up being with the overall language; Links?

My point was about the inability to encode abstractions, which you loose out on without sum types and generics.

I've been meaning to read the 1ML paper for some time. Of course, it is possible to have a better power-to-complexity ratio with better design and good research; but this does not apply to Go.

:frowning:

They do, yes. But many of these are extremely useful and used in practical applications.

I am sure there are; but specifically which orthogonal features? -- some concretion would help.

I view throw not in isolation, but as a natural part of the try { .. } feature;

We discussed this on the impl Trait tracking issue. :wink:

1 Like

It may be interesting to read some of Graydon’s opinion on the matter, as the original creator and long-time head of the project- he very much is worried about feature creep in Rust, even for the things I find to be “filling in holes.”

For example, here are two Twitter threads he participates in. Apologies for Twitter’s horrible UI that makes following tree-structured conversations difficult (basically you have to click on each tweet to see all the replies to it):

And most explicitly, there’s these two tweets, from the first thread, from which you could basically extrapolate my entire argument:

4 Likes

This is a delayed response to an earlier post in this thread by @mgeisler.

[pardon the rambling prologue]

As I’ve posted before (here and here), my initial foray into Rust led me to sketch an RFC to add lexemes for wrapping_add/sub/mul and rotate_left/right operators and their assign variants. Because rustc is open source and I’ve considerable experience with compiler internals, I downloaded a copy of the rustc source and worked out the code changes needed to add those operator lexemes and their corresponding traits to rustc. (I did not attempt to develop all the needed documentation changes.)

Once I’d concluded that such operator lexeme additions were feasible, I investigated the impact that they would have on both the rustc source and on the source of the “crypto” category crates on crates.io (since the latter are presumably the heaviest users of wrapping arithmetic). As I reported in the first-cited reference, in that corpus only 5% of the add and subtract operators were of the wrapping variety, which strongly suggested that do nothing was a viable choice.

That conclusion led naturally to consideration of what can be considered best practices if the language is not changed. Investigation determined that other authors had already found an approach that is in many ways superior to the “extend the language” alternative.

From this experience I concluded that any RFC for a language change should include both

  1. statistics on how widely the change would improve a sizable representative corpus of existing source code, and
  2. discussion of “best practice” alternatives if the change is not made.
16 Likes

I would not concede that is a bad thing. In fact, I would argue the opposite. If I, as the crate author, find it important enough to change the names of the parameters of a function/method, then, it must mean that there is some semantic that needs correcting; otherwise, why change it? If there is a new semantic, then, I want that to fail on compile for existing code so they know to review the semantic change and ensure their code is appropriately updated.

Could you clarify what you mean by "Brittle"?

It seems like they are being considered very carefully. It seems to me that more proposals, pre-RFC's, RFC's are shot-down than are accepted so I'm not exactly sure what you are arguing other than you don't like some particular features that were accepted. Is your definition of good, "What I like?" Seems a little short-sighted.

So recently, we were doing some improvements to frunk (this included breaking changes elsewhere, but let's assume it didn't) including improving the readability of code. This included renaming method parameter names to something more readable (specifically not one letter names). If the parameter names were part of the signatures, we couldn't have made this improvement.

Of course there are also benefits to making parameters part of the signature, which @mgeisler outlined.

Good question! Generally speaking, I mean "breaks easily" (fragile is probably a better word);

Specifically, I mean that positional syntax would break if the field order is changed in the struct (this has no effect on memory layout). It also unclear without having the order of the fields exactly in short term memory what the field order is -- you have to go look it up if you don't remember. The syntax Foo { field_a, field_b } requires less of your short term memory because the bindings you are moving into the struct literal are more likely to be close in visual scope (unless you have a super long method, but then you have other problems as well..)

1 Like

See, I would, with all respect to your opinion on the matter, argue the opposite. I would make the change, bump the semver, put in the change-log/docs a comment about steps to upgrade to the new version that would include a simple search and replace of the parameter names. If I were really worried about it, I'd create an assist crate that would compile to a cargo plug-in/command that would perform the necessary upgrade on any code-base it was given. I just don't see why renaming parameters should not be a potential breaking change. I also don't see why that isn't OK. Of course, on something as subjective as this, opinions will vary.

1 Like

@gbutler You’re actively proposing a new feature. Consider opening a dedicated thread for this, so that it doesn’t get lost in the discussion.~

Edit: @gbutler I mixed you up with @mgeisler Disregard that comment

1 Like

@MajorBreakfast: I think you may have misread. In this discussion, I’ve only responded to and asked for clarification on other’s comments. I’m not aware of anywhere, in this thread, that I’ve proposed anything.

EDIT: Or were you referring to my comment, “If I were really worried about it, I’d create an assist crate that would compile to a cargo plug-in/command that would perform the necessary upgrade on any code-base it was given”? If so, you may be right. I wasn’t proposing a change in my mind, but, on reflection, this could be a nice feature. A Cargo command that performs mechanical upgrades/refactorings of various, well-defined types of changes between Crate versions ABI. It might include things like:

  • renamings
  • reorderings (parameters, struct members, enum elements (especially C-like))
  • retypings (probaby harder, would need some thought)

Perhaps this could have some sort of meta-language to define the refactorings necessary between two semvers, with, perhaps auto-generation attemptable. Would need some thought I guess.

Is that what you meant by be “…proposing a new feature”?

Fair enough; As long as we are clear eyed about the trade-offs.

Personally I wouldn't do it; but there are certainly good reasons why you might want to.

You're right in principle, but it's not always 100% clear cut -- in the Python world, projects often explicitly document if keyword arguments are part of the public API.

Most often, keyword arguments are used together with default values for the arguments. If Rust had that feature, there is a forwards compatible way to rename the arguments: keep the existing names around and let the function reconcile the old and new parameters when called. That's of course not super pretty, but API changes rarely are :slight_smile:

Interesting :slight_smile:

Thinking about defaults separately, my personal preference would be to solve this like so instead:

fn foo<x: Option<usize> = Some(1)>(y: usize) { .. }

foo::<None>(1) // x = None, y = 1
foo(2) // x = Some(1), y = 2

EDIT: Think const generics like const N: usize, but without the const in front. :wink:

Moving towards dependent types:

fn index<A, length: usize>(idx: usize, vec: Vec<length, A>) -> A
where idx < length {
    ..
}

index(1, my_vec)
index::<_, 3>(3, my_vec_3) // TYPE ERROR! 3 < 3 does not hold.

This solution is inspired by Idris and Agda.

Thanks, but I didn't really mean to propose anything -- I was just pondering what could have been with regards to keyword arguments and how it would have made everything very well aligned if the struct initialization short-hand hadn't been introduced.

I was trying to point out how that feature feels like it doesn't belong with the rest of the language (based on my somewhat limited experience).

Also, I'm sure the mechanics of default values for function arguments is well-known to the language team -- lots of languages have this feature now. It would be nice if there were an overall coherent story here and if things were nicely symmetric across the language.

2 Likes

Speaking both personally as a member of the Rust community, and with respect to work on the language team (though not speaking for the language team): this is a very real concern, and I share it myself. I tend to push back heavily on quite a few proposed language features, and I think we should have a high bar for making the language (as opposed to the libraries) bigger.

Adding more surface area to the language has a cost. That cost often doesn’t get seen until long after adding the feature; it’s the accumulated weight of language features that people have to understand in order to read other people’s code. It’s a cost in maintenance, across the ecosystem.

When we choose to add new language features, it shouldn’t just be because they make code more concise, or more expressive. It should be because, long-term, having the feature makes code not just easier to write but easier to read, understand, and maintain.

Avoiding duplication and redundancy can make code easier to maintain, because it avoids having to keep two copies of something in sync. Having ? makes code easier to read and understand left-to-right (x.foo()?.bar()?), whereas try! required bouncing back and forth between sides of an expression (try!(try!(x.foo()).bar())).

When we add a new language feature, we should make sure we’re asking the right questions. “Does this make code easier to read, understand, and maintain?” is far more important, and gets asked less often. That holds especially true when we give serious consideration to the alternative of “don’t do this at all”, which we should always do.

One notable point to consider: in most languages, it’s a lot easier to provide libraries than it is to change the language itself. That creates a natural pressure to keep the language small and add to the library instead. In Rust, the RFC process makes it relatively straightforward to propose changes, whether to the language, libraries, or tooling, and I think that’s a feature. But given that, we should still keep that same premise in mind: small language, larger libraries.

Inspired by this and by various other recent discussions of proposed language features, I just proposed some changes to the standard RFC template to encourage thinking along these lines: https://github.com/rust-lang/rfcs/pull/2432

12 Likes

Personally, I'm strongly against mixing nominal and positional. Braced struct fields have names, so they are only ever referenced by those names, such that order doesn't matter. Tuple struct fields don't have names, so are only ever referenced by their positions, so order matters greatly.

(There's one exception to "order doesn't matter for nominal" in safe rust, but that's a different rant that I'll skip for now.)

6 Likes

I was making a distinction between ordered comma-separated lists and unordered key-value pairs. The first is what I think of when I see T{x, y, z} since it doesn't have the distinctive key: value syntax at all.

Also, I was kindof thinking of the fields of a tuple as being named 0, 1, 2, and so on. That mental model seems to fit with how you access the fields: x.0 and x.1 -- analogous to x.name and x.age for a braced struct.