Back to Csharplang

C# Language Design Meeting for Dec. 11, 2019

meetings/2019/LDM-2019-12-11.md

latest5.5 KB
Original Source

C# Language Design Meeting for Dec. 11, 2019

  1. Design review feedback

Discussion

We got feedback from the design review that we shouldn't try to conflate too many problems. If we want to make it easier to support structural equality, we should see if it's possible to address directly, without requiring the other features of records. One suggestion was to take inspiration from VB, which allows the key modifier to be added to VB anonymous types to indicate structural equality with the members used as the keys.

We took that advice and looked at a sketch of what that could look like:

C#
class C
{
    public key string Item1 { get; }
    public string Item2 { get; }
}

The key modifier would be used to control generated equality, such that all members marked key would be compared for equality in an Equals override (using the same pattern as in the original records proposal).

The above code sample certainly looks simple, but unfortunately it's not sufficient for real-world code. Both Item1 and Item2 are get-only autoproperties, meaning that as-is there is no way to initialize those members. A working example looks more like:

C#
class C
{
    public key string Item1 { get; }
    public string Item2 { get; }
    public C(string item1, string item2)
    {
        Item1 = item1;
        Item2 = item2;
    }
}

This is significantly longer than the original sample, grows with the number of properties, and is repetitive. The latter is particularly problematic, as repetitive boilerplate is often a source of hard-to-see bugs or typos.

Worse, we've seen that when construction becomes laborious, users resort to making their types mutable instead of writing out the constructor, e.g.

C#
class C
{
    public key string Item1 { get; set; }
    public string Item2 { get; }
}

This is unfortunate in most code, but it's worrying when combined with structural equality. When used in Dictionaries, mutable types with structural equality are an anti-pattern because the hash code of the type changes, causing the type to "disappear" from the Dictionary. It's one thing if the user opts-in to this risk, knowing that they need to be careful, and a completely different situation if the language encourages a dangerous pattern.

Conclusion

This is an interesting design point that we think we'll incorporate. We've also agreed that we have a hard design requirement: if we provide a feature for easy structural equality, we must provide a more convenient syntax for constructing immutable types in the same release. To do otherwise would be to effectively make a trap for users.

Nominal vs Positional

We also continued to explore the space of nominal vs. positional records. An insight from the previous discussion is that nominal vs. positional records are really about improving syntax for type construction. Positional records are about taking the existing type construction, constructors, and giving them a shorter syntax. Nominal records are about identifying some weaknesses of the existing construction system and providing a new feature to support them. In both cases, though, the proposed features shorten construction by avoiding repeating the member declarations and the assignments.

We also had the following notes, in no particular order:

  • For positional constructors, it's important to consider primary constructors. We have one proposal for primary constructors, but have not had a discussion on whether we want them, and if the syntax is worth taking away from the record syntax.

  • If the initonly keyword is required, even for common cases, this is about as expensive syntax-wise as a setter. It may be a few more characters, but it keeps things on one line, as opposed to the multi-line constructors that are required right now.

  • There's an opposition between evolving existing data types and picking and choosing features when you need them, but also providing a simple syntax for the most common case. Certainly positional records solve a common case. The question is how common that case is.

  • Positional records use existing initialization strategy (construction) which is fairly well-understood. The initonly feature, by contrast, will force us to reexamine some assumptions. For instance, constructors take all inputs at once, meaning that you can enforce requirements between them at construction time. initonly overrides properties after construction, and one-by-one, so it's not possible, or not obvious, how to provide this functionality.

  • There are mixed feelings on the requirements of what features will be available individually, and which are separable. Some people feel that addressing the most common case is sufficient for providing the value of the feature, namely that there can be a single "record" which provides immutable construction and value equality, and that is the only way to access these features. Others think that the features need to be adoptable independently: existing types need to be able to adopt generated equality without adopting immutable construction, while immutable construction needs to be available without structural equality.

One conclusion is that no proposal will solve all record-related problems. This is fine. We also have general agreement that there should be a convenient shorthand for the combination of most or all of the features (simple immutable construction, structural equality, etc.). Notably we don't all agree on whether or not structural equality will be the most common type of equality for records, but the shortest record form may include structural equality for other language design reasons.