124 lines
6.8 KiB
Markdown
124 lines
6.8 KiB
Markdown
|
|
||
|
# 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?
|
||
|
|
||
|
|
||
|
|