# C# Language Design Meeting for July 26th, 2023

## Agenda

- [Primary constructor parameters and `readonly`](#primary-constructor-parameters-and-readonly)

## Quote of the Day

- "`record`s, the other white meat"

## Discussion

### Primary constructor parameters and `readonly`

https://github.com/dotnet/csharplang/issues/2691  
https://github.com/dotnet/csharplang/issues/188  
https://github.com/dotnet/csharplang/discussions/7377  
https://github.com/dotnet/csharplang/blob/bc2c69e6f2bd28373e341effaa1182ebcb7ea72c/proposals/readonly-parameters.md

Continuing on from [last Monday](LDM-2023-07-17.md#readonly-parameters), we took a look at the broader space of primary constructor parameters,
their default mutability, and how we want to address community feedback in this space. With the exception of `!!`, this is the most feedback we've
ever gotten on a preview feature, and if we want to address this in any way other than adding a `readonly` modifier at a later date, we need to
decide that very soon. Broadly speaking, the feedback we've seen is requesting that users want to be able to be confident that type state is not
being modified, and that the compiler will help them enforce this. We see a few ways of addressing this:

1. Do nothing. Leave primary constructors as mutable, and do not plan on addressing this. If a user wants to enforce readonly class state, they should
   declare a class member (field or property), explicitly assign into that member, and use that member, not the parameter.
2. Plan on adding a `readonly` keyword. This keyword may come with C# 12, but might also be delayed until C# 13. There are open questions as to
   what this keyword would mean: would it follow field `readonly`, where the parameter capture would actually be mutable during initialization, or not?
   But by default, primary constructor parameters would always be mutable unless otherwise specified.
3. Follow the default established by record primary constructors. This would mean that all primary constructor parameters would be mutable during
   initialization (as in all `record` types today), but for `class`es and `readonly struct`s, they would be readonly after construction.
4. Have all primary constructor parameters on non-record types be always `readonly`.

This is a big list of options, and worse, they come with various correspondences to existing things in the language. Option 1 is parameters as they
exist today: they're always mutable, for better for or for worse. Option 2 corresponds with thinking of primary constructor parameters as baby type
members: they start with the same set of defaults that normal type members have, and then we might at some point add the ability to promote them inline
to full type members, with all the same defaults that other type members get. Option 3 corresponds to records as they exist today, and if we take an
opinionated stance on primary constructor parameters now, it might allow us to continue to take an opinionated stance for future features more easily.
For example, maybe `public int I`, as a primary constructor parameter, could always mean a `get`-only property. Option 4 is the one the stands out here,
as it doesn't really correspond to existing language principles in any way today. It is a position that many LDM members might wish we'd taken from the
start of the language, but isn't really in line with any prior art in the language, which we feel would be a negative surprise.

Any form of readonly by default is proving to be a contentious topic. There is a nasty footgun that comes with this for mutable value type parameters,
where silent copies can occur without an explicit `readonly` somewhere in the user's code. This is something that can bite even experienced users with
today's semantics where `readonly` must be opted into, and it seems likely that a `readonly`-by-default decision might make that worse. At the very
least, we may want to consider a warning when calling a non-`readonly` member on a primary constructor parameter.

Another point of concern is when `readonly` comes into force. It seems likely to us that some amount of validation and value clamping may want to
be done as part of initialization; should that require declaring a full member? Or should the parameter be mutable during initialization, so that
validation steps can occur? This is more of a future correspondence question than it is a concern for C# 12/13; if we do choose to allow `readonly`
primary constructor parameters to be modified during initialization, that sets a precedent for what `readonly` on a parameter means. This then informs,
and potentially harms, the ability to use `readonly` as a general parameter modifier later on in the language's lifetime, since it would have a
different meaning there. While the LDM's opinion on that feature hasn't changed, it is something important to consider.

We also looked at the topic of boilerplate, and how much we would harm the general reduction of boilerplate by making things `readonly` by default. We're
pretty universally against adding a new `mutable` keyword to C#; that would mean that, in order to get mutable state, users would be required to fall back
to using a full field. In the other direction, in order to have `readonly`ness in a feature where mutable parameters are the default, users would be required
to put `readonly` on the parameter, just as they are required to do for fields. While we do think that more object state is effectively readonly than is
not, some members are concerned that enough state is mutable (and actually mutated) that the `readonly`-by-default solution would not help as many users
as the mutable-by-default version. Importantly, this isn't just a war on boilerplate for the sake of brevity: there is mental tax that comes with repetitive
boilerplate that we would like to avoid.

Another argument (yes, there's still more to go) in the future evolution space was how far we're prepared to go with primary constructor parameters. We've
looked at promotion to members: what about when those members need to have more complicated logic? Where is the dividing line between "This can be a
primary constructor parameter" and "this must be a full member"? We don't want to ship something that needs a decoder ring to understand, which is something
that we're concerned about if we did option 3 and then allowed promoting to full type members. We think we'd have reasonable and understandable defaults for
`private` and `public`: `private` fields, `public` properties. But we are worried about `protected`; sometimes they're effectively part of the public
API, and properties are the best choice. Other times, they're effectively `private protected`, and fields are the better choice. There's a potential soup
of modifiers and features here that could be extremely confusing to readers, which is something we want to be very careful of.

Considering all of this, we are about split between options 2 (add `readonly` as a modifier) and 3 (match records). This means that option 1 (do nothing)
and option 4 (always `readonly`) can be excluded. We think we need a few days to consider these options before coming back as a group on Monday to make a
final decision.

#### Conclusion

We've eliminated two options. We'll come back Monday to conclude on this topic.
