csharplang/meetings/2020/LDM-2020-02-26.md
2020-03-24 00:19:58 -07:00

6.8 KiB

C# Language Design Meeting for Feb. 26, 2020

Agenda

Design Review

Discussion

Today is a design review, where we collect the design team, selected emeritus members, and a number of broad ecosystem experts to provide some "in-the-moment" feedback to our current designs and their directions.

Today we presented more of our thoughts on the top-level statements/"simple programs" features and records.

Simple Programs

We have a prototype of simple programs. As per the existing spec, you can have top-level functions among all files, and other statements in a single file.

Collected feedback, not in any particular order:

  • Supporting local functions in files other than the top-level statement file doesn't seem useful and could cause confusion. If there's a local function defined in a file only with classes, it would appear that that function would be in scope for the classes, according to C# lexical scoping conventions. However, since these are defined as local functions, not top-level methods, it would be an error to use them inside the class. Moreover, even if that confusion is resolved, there doesn't seem to be a compelling reason to allow it in the first place. Because these functions are not accessible from anything except the main statement file, it would be most likely to want to put the functions in that file, next to the uses. The only benefit from allowing local functions in separate file may be as a helper file that is linked into other compilations. However, wrapping these utility functions in a class so they can be used in more places seems like a small burden with big benefits. Once wrapped in a class, these functions are simply methods like in C# today.

  • Mixing classes and statements in the same file could generate some confusion. The existing design is that classes can see the variables created by statements, but it would be illegal to reference them. This keeps the option open to allow access later. However, this could be a confusing middle ground. To simplify the situation we could require only statements in the top-level in one file (forcing all types to be declared in separate files). However, there is interest in using utility classes in the top-level statements, perhaps especially with a forthcoming records feature that provides simple, short syntax for declaring new types.

  • Many of these features mirror what we already have in CSX. It's good that our current designs are similar and allow these constructs in more places, but since the semantics of this design have subtle differences from CSX this would effectively create a third dialect of C#. There's some desire to unify these worlds, but it's difficult. CSX is designed to allow all values to be persisted, which is important for the scripting "submission" system, but this makes a number of types of statements illegal that we have support for in the current design, like ref locals. It also creates a burden for new designs, where statements need to be explicitly designed for both CSX and C#. For example, the new using var declaration form is nonsensical under the CSX design and probably should be illegal. Since the current 'simple programs' design effectively treats statements like they are part of a method body, there's a cleaner semantic parallel with C#, meaning less special-casing.

Records

Here we presented a variety of different pieces of designs we have been thinking about.

Nominal Record

Feedback:

  • One of the biggest drawbacks of the current writeable-property style in C#, where types are declared with public mutable properties that are then initialized using object initializers, is that author can't enforce that certain properties are always initialized. It would be a big disappointment if any "nominal records" feature that we built couldn't support this feature.

  • With the design as-is there's no way to validate the whole state of the object. However, that's also true of the object-initializer style currently in use, and this doesn't seem to be as a big of a problem for current users.

Value Equality

  • Positive feelings on generating .Equals(T) and implementing IEquatable<T>, mixed feelings on generating the == and != operators.

  • If we opt-in the whole class using value class, we also need an opt-out for individual members. Regular classes also often have somewhat specialized equality requirements, like wanting to compare certain lists as sequence-equal, or compare strings ignoring case. This observation points to a lot more customization points for value equality on general classes than value equality on records.

  • Using value equality on mutable state is seems dangerous if the type is used in a dictionary, but despite the danger, other languages (Java, Go) have value equality for common types, like lists, that can be easily added to a dictionary.

  • We don't currently have a robust mental model for what it means to be a "value class." Is "value class" a separable concept from "implementing value equality," which people often do today? Or is it not a different type of class, but simply a modifier signaling an implementation detail, namely that the compiler generates value equality automatically. If we think of value equality as a public contract, how does that change our view of existing code? Classes can currently override Equals, but we don't distinguish what kind of Equals they provide. That isn't a language concept, in a sense, but a part of the documentation.

Nominal Records

  • When using the with expression on nominal records, the generated parameter-less With method looks a lot like Clone. It does little aside from return a new object with a shallow copy of the state. If With is essentially Clone, why not use one of the existing forms of Clone that we already have?

    • ICloneable is deprecated and MemberwiseClone is protected. Maybe we should just call the method Clone(), but not override or implement any of the other framework uses.
  • This feature looks a lot like structural typing from other languages, like Javascript's "spread" operator, but that is not the feature we're currently trying to build. This is feature is still about declaring new types, not providing some compatibility between existing types.

  • We spent a lot of time talking about validation and "validators", a very recent concept that was floated as an alternative to constructors, executing after the with expression.

    • There's some general concern about having no capability of validation, but no consensus on exactly how validators should work.

    • If validators work against the copied fields of the object, that seems to imply that the fields are the state being operated on. On the other hand, only certain members can be mentioned in the with expression. Why wouldn't those be the things that are copied? Instead of all the state?