
# C# Language Design for Jan. 29, 2020

## Agenda

Record -- With'ers

## Discussion

In this meeting we'd like to talk about Mads's write-up of the "With-ers" feature, as it relates
to records. Multiple variations have been proposed, but the suggestion generally takes the form
of a `with` expression that can return a copy of a data type, with selective elements changed.

Write-up: https://github.com/dotnet/csharplang/issues/3137

The first thing we learned is that the fundamental problem we're trying to solve is
"non-destructive mutation."

There are two approaches we've thought of: direct copy and then direct modification, and creation of a new type based on the values of the old type. 

1. Direct copy. We might call this "copy-and-update" because we copy the new data type exactly,
then update the new type with required changes. The basic implementation would be to use
MemberwiseClone, and then overwrite select properties.

2. Create a new type. we call this "constructing through virtual factories." If the type supports
a constructor, this approach would call the constructor using the new values, or the existing
ones if nothing new is given. The construction would be virtual so that derived types would not
lose state when called through the base type.

There are advantages and disadvantages to each proposal.

(1) is simple but seemingly dangerous. There are often internal constraints to a type which must
be preserved for correctness. Usually this is enforced through the type constructor and
visibility of modification. That would not necessarily be available here.

(2) does construction similar to conventional construction today, so it doesn't introduce as many
safety concerns. On the other hand, the contract looks a lot more complicated. To make the
feature seem simple on the surface, it looks like we imply a lot of implicit dependency. For
example,

```C#
public data class Point(int X, int Y);
var p2 = p1 with { Y = 2 };
```

Would generate

```C#
public class Point(int X, int Y)
{
    public virtual Point With(int X, int Y) => new Point(X, Y);
}
var p2 = p1.With(p1.X, 2);
```

The first requirement is that an auto-generated `With` method must have a primary constructor, in
order to know which constructor to call. Alternatively, we could have a `With` method generated
for every constructor, although that would require a syntax to signal that `With` methods should
be generated in the absence of a primary constructor.

The compiler also needs to know that the `X` and `Y` parameters of the `With` method correspond
to particular properties, so it can fill in the defaults in the `with` expression. Otherwise we
would need some way of signifying which of the parameters are meant to be "defaults":

```C#
public class Point(int X, int Y)
{
    public virtual Point With(bool[] provided, int X, int Y)
    {
        return new Point(provided[0] ? X : this.X, provided[1] ? Y : this.Y);
    }
}
var p2 = p1.With(new bool { false, true}, default, 2);
```

We also need to figure out which `With` method to call at a particular call site. One way is to
construct an equivalent call and perform overload resolution. Another way would be to pick a
particular `With` method as primary, and always use that one in overload resolution.

This also has some of the same compatibility challenges that we've seen in other areas.
Particularly, if you add members to the record, there will be a new `With` method with a new
signature. This would break existing binaries referencing the old `With` method. In addition, if
you add a new `With` method, the old one would still be chosen by overload resolution, if
overload resolution is performed, as long as unspecified properties in the `with` expression are
default values.

On the other hand, this is also a general problem with backwards compatibility overloads. We'll
need to investigate whether we want to add a general purpose mechanism for handling backwards
compatibility and if we want to introduce a special case for With-ers specifically.

What all of the above interdependency implies is that we need a significant amount of syntax or
"hints" about what to do during autogeneration. We previously expressed interest in providing
orthogonality for as many of the "record" features as possible. A conclusion is that
auto-generated With-ers require or suggest many of syntactic and semantic components of records
themselves. When we try to separate the feature entirely, we require user opt-in to specify the
"backing" state of the With-er. This seems to imply that auto-generation should
not be a general, orthogonal feature, but a specific property of records.

However, we don't have to give up orthogonality entirely. The requirements for auto-generated
With-ers doesn't imply anything about manually written With-ers. Auto-generation seems possible
in records because the syntax ties the state to the public interface. Manual specification looks
just like the components of records that can be written explicitly in regular classes, like
constructors themselves. If we do want to pursue this avenue, we should try to limit
the complexity of the pattern as much as possible. It's not too bad if it's fully
generated by the compiler, but it can't be very complicated if we want users to write
it themselves.