112 lines
No EOL
5.5 KiB
Markdown
112 lines
No EOL
5.5 KiB
Markdown
|
|
# C# Language Design Meeting for March 30, 2020
|
|
|
|
## Agenda
|
|
|
|
Records
|
|
|
|
1. Builders vs init-only
|
|
|
|
2. Possible conflicts with discriminated unions
|
|
|
|
3. Value equality
|
|
|
|
## Discussion
|
|
|
|
### Builders vs. init-only
|
|
|
|
We discussed more of the tradeoffs of using builders vs using an "init-only" feature for records,
|
|
and looked into requirements for other languages, including VB and F#. Based on our current
|
|
designs, the work needed to consume the new features for "init-only" seem fairly small.
|
|
Recognizing the `modreq` and allowing access to init-only members, and calling any necessary
|
|
"validator" on construction, are pretty simple features for VB. VB already has the syntactic
|
|
requirements for the feature (object initializers), and we'd like to keep it possible for VB to
|
|
consume major changes in the API surface, if not write those new features. F# is undergoing
|
|
active development and changes are certainly viable there. Because the scope of changes is more
|
|
open-ended in F#, it's possible it could feature more implementation work.
|
|
|
|
Notably, most of the features associated with "init-only" and "validators" do not require a new
|
|
runtime, only a new compiler version. The new compiler version is necessary to recognize safety
|
|
rules (validators must always be called after constructors, if they exist), but they don't use
|
|
any new features in the CLR. The only feature potentially requiring runtime features is
|
|
overriding a record with another record, which could potentially require covariant returns.
|
|
|
|
The remaining differences seem to come down to whether you can "see" the whole object during
|
|
initial construction (as opposed to validation). If you can see the whole object immediately,
|
|
that makes writing a `With` method that avoids a copy if all the values are identical very
|
|
straightforward. However, this could be done for "init-only" as well, by moving this semantic to
|
|
the `with` expression, optionally comparing the arguments to the `With` expression with the
|
|
values on the receiver object and avoiding calling With if they are identical. Therefore we don't
|
|
think we're actually ruling anything out by going down the "init-only" route.
|
|
|
|
There are advantages in going down the "init-only" route instead. The performance for structs
|
|
is probably better and more optimizable, and the IL pattern seems clearer and less bloated.
|
|
|
|
**Conclusion**
|
|
|
|
We like the "init-only" IL better. Given the path forward for other languages and compatibility
|
|
with many runtimes, we think it's a better future as an IL pattern.
|
|
|
|
### Conflicts with discriminated unions
|
|
|
|
There was a general question if these decisions could impact a future discriminated unions feature.
|
|
We don't have a lot in mind, but if we do end up building a discriminated union made of the records
|
|
feature, there is one component we may want to reserve. Discriminated unions often have a set of simple
|
|
data members. For instance, if we wrote a Color enum as a discriminated union, it could look like.
|
|
|
|
```C#
|
|
enum class Color
|
|
{
|
|
Red,
|
|
Green,
|
|
Blue
|
|
}
|
|
```
|
|
|
|
If we reduce these to records, it may look like:
|
|
|
|
```C#
|
|
abstract class Color
|
|
{
|
|
public class Red() : Color;
|
|
public class Green() : Color;
|
|
public class Blue() : Color;
|
|
}
|
|
```
|
|
|
|
The problem is: since those classes are effectively singletons, we'd like for the instances to be
|
|
cached by the compiler, to avoid allocations. However, we don't currently have a syntax in C# that
|
|
means "singleton." Scala uses the "empty record" syntax to mean singleton. We need to decide if we'd
|
|
like to reserve that syntax ourselves, or find some other solution.
|
|
|
|
### Value equality
|
|
|
|
We've decided that we want value equality by default for records. We need to settle on what that
|
|
means. The primary proposal on the table is shallow-field-equality, namely comparing all fields
|
|
in the type via EqualityComparer. This would match the semantic we have decided on for "Withers,"
|
|
where the shallow state of the object is copied, similar to MemberwiseClone.
|
|
|
|
There's a large segment of the LDM that thinks doing anything except for field comparison is
|
|
problematic because it introduces far too many customization points for the record feature.
|
|
Almost all custom equality would have to deal with sequence equality and string equality, which
|
|
are already very different mechanisms. There's a proposal that we could provide further
|
|
customization via source generators, which could allow almost any customization.
|
|
|
|
A follow-up to that is: why have value equality by default at all? Why not use source generators
|
|
for all value equality? One problem with that is that we want the simplest records to be very
|
|
short. The other problem is that we effectively have to pick an equality (C# will inherit one if
|
|
you don't). We previously decided that value equality is a non-controversial default -- if it's
|
|
wrong it's probably not worse than when reference equality is wrong, and it's often better.
|
|
Struct-like field-based equality is a simple and familiar version of value equality.
|
|
|
|
We also need to decide on value-equality as a separable feature for regular classes. Some people
|
|
like the idea of a separate feature and would be fine even with the constrained version that only
|
|
compares fields. Others don't think this feature meets the hurdles for a new language feature. If
|
|
we don't have customization options, it may be rarely used, and it seems possible that a source
|
|
generator version could be the much more popular version. It's also worth noting that, as a
|
|
separable feature, we don't need to add separable equality now.
|
|
|
|
**Conclusion**
|
|
|
|
No separable value equality for non-records, right now. Default record equality is defined as
|
|
field-based shallow equality. |