[Pre-Pre-RFC] The ..Default::default() shorthand is misleading

That's true, but the examples you are giving here are widely used in many programming languages. The struct update syntax in its current form is specific to Rust as far as I know.

This is the same reason as why we usually don't write let world: World = World::new(), but just let world = World::new(). We know the type of world already by looking at World::new(). Also, this reduces the work done in refactoring and keeps the code generic. If we want to change the return type of World::new() to e.g. Universe, then we have to do a minimal amount of changes.

In my experience, using the most generic statement possible has worked very well in Rust, and I never had any problems until I stumbled upon the struct update syntax. In this case it might actually make sense to be more explicit to avoid ambiguity.

In this case the same meaning (type constraint) is not repeated. It is just a coincidence that the trait is called the same as its only method.

I think we will never be able to beat the power of a global search engine when it comes to information retrieval, and forcing people to use two information retrieval services rather than just one would probably not be good.

[quote="tczajka, post:18, topic:15917, full:true"]

You are right that we cannot be sure what other people are thinking. However I can hardly imagine that the three other people that complained about the missing warning when doing the exact same thing I did in the opening post did it on purpose. They could have of course tried it to check if there is a warning about the recursion, maybe they were just curious?

But to gather some further evidence, I made a poll in the Rust users forum, checking if people get the same misunderstanding from this code that I got.

And just to be safe: When I assume that other people have made a mistake during their code development process, I do not see that as a weakness of that person. I think making mistakes is very natural, especially in complex tasks such as software development. My motivation is not to point the finger at other people, but to investigate a potential weakness in the readability of Rust code. And well, the main characteristic of a weakness in the readability of code is that people make mistakes while reading it.

Is the reason for this known? Seems like a warning bug to me -- it seems obvious to see that it's still unconditional infinite recursion.

5 Likes

Here is the corresponding issue: `unconditional_recursion` lint doesn't work with struct update syntax · Issue #78474 · rust-lang/rust · GitHub

7 Likes

Actually I agree, I think using Default::default() and type deduction here is not unreasonable, there are pros and cons of both ways.

The benefit is somewhat less clear than let world = World::new() because in this case you can simply omit something rather than replace it.

I think a better analogy is using Self in impls for specific types, which is also a reasonable style.

If we're doing statistics, you gotta say: three incorrect uses out of how many total? People make all sorts of mistakes, from time to time.

3 Likes

Note that

World {
    tree: Tree::grow(),
    ..Default::default()
}

does not desugar to

{
    let tree = Tree::grow();
    let mut world: World = Default::default();
    world.tree = tree;
    world
}

but instead to something like

// this desugaring uses `match` to get the scopes
// of temporary variables right
match Tree::grow() {
    tree => match Default::default() {
        World { lake, mountain, .. } => World {
            tree,
            lake,
            mountain,
        }
    }
}

In particular, it only moves (or copies) certain fields out of the Default::default() expression. E.g. compare this Rust Playground with this Rust Playground.

5 Likes

It's similar to how the spread syntax is used in object literals in JavaScript: Spread syntax (...) - JavaScript | MDN. I always assumed that the Rust syntax was inspired by it, but looking into it now it looks like the Rust syntax actually predates it.

I think explaining the ..Default::default() use case in the section of the book that introduces the update syntax and working to improve the lint situation would be sufficient.

Which, incidentally, leads to surprising stuff related to privacy: IIRC: assuming a private field (other than tree) in World,

  • one can still use World::default().also(|it| it.tree = Tree::grow()),

  • but can't use World { tree: Tree::grow(), ..<_>::default() }

3 Likes

Sorry but that's a directed question which is begging the answer. One can't reasonably expect people to magically guess the behavior of a piece of syntax they don't know about. The solution to this is not adding more syntax or changing syntax; it is documentation and education.

No.

I find it really quite inappropriate for multiple reasons.

  1. It's plain unnecessary. It is not any more intuitive than current FRU with .., yet it performs the same action, so it's 100% redundant.
  2. It is problematic with regards to parsing, because it's multi-token, whitespace-separated, and infix, so precedence needs to come into the picture. There's no need for more infix operators in the language. Just no.
  3. While it doesn't matter much, I find it subjectively ugly, because multi-word full-phrase-like operations are unprecedented in this language. Maybe it would fit in SQL, but definitely not in Rust.
12 Likes

Thank you for your contribution.

I would be happy if you would keep in mind that there are humans on the other end of the wire.

Also it might help to read a bit more through the discussion, take a step back, think about it, and then formulate your thoughts a bit more clearly and neutrally.

edit: I have taken offense here under the assumption that you called me a beggar. However, as a friendly moderator explained to me, you more likely referred to the logical fallacy begging the question. This is actually something I can argue about and will do in a separate reply. However I would like to encourage you to refer to logical fallacies using more neutral terms. For example, the term "assuming the conclusion" does not contain any emotionally loaded words like "begging", and names the thing by what it is. That would at least make things easier for me, and given that we are in an environment of people with vastly different cultural backgrounds, likely also some other people.

As usual, I'm pretty sure Rust got it from Ocaml.

At last, it is possible to update few fields of a record at once:

# let integer_product integer ratio = { ratio with num = integer * ratio.num };;
val integer_product : int -> ratio -> ratio = <fun>

With this functional update notation, the record on the left-hand side of with is copied except for the fields on the right-hand side which are updated.

7 Likes

I have apparently misunderstood your intent. If it does the same thing, then it would clearly still be unconditional recursion if used in a Default implementation.

I had incorrectly assumed that you wanted a way to express “use the field defaults, except for these ones” in such a way that it would work in a Default implementation. One reason I thought that was desired was that more than once I have found myself changing a struct to add a field of a type which, (for good reason,) does not implement Default, but for which there is a good default value for my particular use case, and found myself annoyed at needing to write out setting all the rest of the fields to their defaults by hand.

For example, given we start with this example:

#[derive(Default)]
struct ManyFields {
    a: A,
    b: B,
    // ... fields `c` to `x` elided for brevity
    y: Y
}

Then we want to add z: Z, and we need to pick between Z::Zed and Z::Zee. Z::Zed works for us. But since this results in infinite recursion:

impl Default for ManyFields {
    fn default() -> Self {
        ManyFields {
            z: Z:Zed,
            ..Default::default()
        }
    }
}

then we end up needing to write:

impl Default for ManyFields {
    fn default() -> Self {
        ManyFields {
            a: A::default(),
            b: B::default(),
            // ... fields `c` to `x` elided for brevity
            y: Y::default(),
            z: Z::Zed,
        }
    }
}

which is, while not exactly a pressing issue, a bit annoying when it comes up.

1 Like

That's very true. I can speak only for myself by saying that after some 3.5 years of using Rust, this misunderstanding of the syntax was the hardest to clear up until now. The special difficulty is that my alternative interpretation of the syntax made too much sense to me and that I had not seen that kind of syntax before. This is only from my side of course.

That's a good point. This would actually argue well that it makes sense to keep the syntax this way, assuming they are reasonably similar.

Yeah, that is what I tried to achieve when using the struct update syntax without understanding it. But yeah, not what I wanted to argue for here, even though a solution for this would be useful as well.

It's not reasonably similar. To get the equivalent to Rust's struct update syntax in JS, you have to put the spread operation first, otherwise it won't work:

// Rust
Foo {
    bar: Bar::new(1),
    ..default
}
// Javascript equivalent
{
    ...default,
    bar: new Bar(1),
}

The JS syntax is also much more powerful:

  • Objects with different fields can be merged by using the spread syntax multiple times: { ...foo, ...bar }

  • You can define which field overwrites which: { ...foo, bar: 5 } has a different meaning than { bar: 5, ...foo }

However, ... doesn't copy the prototype, so it doesn't work with classes.

2 Likes

[quote="Aloso, post:33, topic:15917, full:true"]

Yes, but all of this properties originate in the fact that js is dynamically typed while Rust is statically typed.

To a large extent spread operator could be made to work with Rust. For example the case of merging different structs could be implemented by mapping fields by name. This doesn't rely on dynamic properties:

struct A { a: i32 }
struct B { b: i32 }
struct AB { a: i32, b: i32 }

AB { ..a, ..b }

It's a bit magic, and more like structural rather than nominal typing that Rust uses, but I would actually find it useful to copy fields from a "builder" struct to a struct it builds.

1 Like

I remember seeing an RFC for that somewhere, but like you said, it's magical (not that I'm personally against this). Naturally it should be the same type.

5 Likes

Yeah, that was what I meant.

Please also note the edit on my first reply to you, as I guess you will not be notified about that edit by the forum software.

So you are saying my question can only be answered with yes, because people not knowing about something means they can only guess randomly, right?

There are two points I would like to make here. One is that syntax can be guessed right without having learnt about a specific piece of syntax. And the second is that even if there is no "logical" way for people to guess what a syntax does, their guesses might still be distributed different than 50:50.

So what is an example of syntax that can be guessed right without even knowing about Rust? I would argue that all the basic arithmetic operations like 10 / (2-3) can be understood by anyone with a bachelor's degree in computer science or related engineering, even if they never heard about Rust before. And we can continue, method calls are also pretty standard between programming languages written by a.b(), and calling freestanding functions by writing simply f(). Also declarations of functions are a pretty standard thing to do in programming, so if someone reads fn x() -> u32 {...} they will probably understand that u32 is a return type, even if they have only seen languages like Java, Python or C++ before, where the return type is notated differently. So for all these examples, my question would be answered with no by most people I would expect. And, assuming that many languanges would use the same struct update syntax as Rust, then many people would also answer "no" to my exact question, stating that many other languages do the same and therefore most people know how it works.

For the second point ("even if there is no 'logical' way for people to guess what a syntax does, their guesses might still be distributed different than 50:50"), which is more what I was arguing in my opening post, I would like to stress that people are biased. These biases can be manyfold and can have many sources, and they are not evenly distributed as a rule. I believe that if we would present an example of the struct update syntax to people that do not know it but do have some programming background, and give them two possible interpretations to choose from (e.g. mine from the opening post, and the correct one), the answers are not as a rule distributed 50:50, but there would be some bias. And I also believe that different syntactic ways of expressing the same thing will produce different biases.

These biases do not come magically, but they come from the biases in the people themselves. For example a JavaScript developer might more likely guess the struct update syntax correctly since there is a similar syntax in JavaScript.

While I understand that designing a sound syntax system for a practical programming language is an immense challenge on its own, I believe that, given that resources are available, designing for these human biases is also a valuable goal.

Let's consider the example of some syntax X in some programming language L, and a poll with a large enough cohort of people that often use L but have never heard of syntax X. If 95 percent of people in that cohort would interpret syntax X wrongly, then I think it would be appropriate to call syntax X "misleading".


Coming back to the matter at hand:

It would be helpful for the discussion if you would give some arguments for that, and/or refute the arguments I made in the opening post.

Remember that the syntax was proposed as a replacement, so it will not be redundant in the long term. About intuitivity, see my next message.

This is a good point. I agree that the originally proposed syntax with two words was not the best idea, having just one keyword would be better. And right, the precedence rules might actually make for a more complicated syntax in the end.

True, two-word operations do not fit into current Rust, and are also not necessary for solving the problem I posed.

2 Likes

No, I am saying that answering "no" will make people uncomfortable, because of how the question is formulated. It "can," of course, technically be answered by "no", but that makes the respondent look silly, since the phrasing itself suggests that it is "obviously" confusing.

This is correct, except that it doesn't support your point. All of these examples actually assume prior knowledge and/or experience of the cited syntax from math or other programming languages. Those who weren't exposed to either programming or math won't know what o.f() or fn x() -> u32 means. It's obvious to us who have experience with ≥1 other languages, but for someone whose first programming language is Rust, it's not any more obvious than FRU.

Same here – I don't see how or why, without actually conducting representative, scientifically sound research on how different syntaxes are distributed, one could claim that a replacement syntax would be an improvement. Based on this argument, it might be better, or it might be worse, or it might be just as good/bad as the status quo. And since you are proposing the change, the burden of proof to show it's a substantial and worthwile improvement (or an improvement at all) is on you. One shouldn't have to go out of one's way to defend the status quo.

I did that by explaining why, in the 3 points in the subsequent paragraph.

In that case, such strongly breaking changes are IMO absolutely off the table.

5 Likes