csharplang/meetings/2020/LDM-2020-01-15.md
2020-02-03 20:58:18 -08:00

4.5 KiB

C# Language Design Meeting for Jan. 15th, 2020

Agenda

  1. Working with data
  2. Record feature breakdown

Discussion

Working with data

As we discuss records, we want to go over a design document we produced a number of years ago called "working with data." This document lays out how, when we design features, we inherently express a "path of least resistance," which consists of the features that seem easiest or shortest to use to accomplish a given problem.

Link: https://github.com/dotnet/csharplang/issues/3107

The document argues that, as we find particular patterns to be more effective at building software, we should make the forms we find to be more effective simpler or shorter to express in the language. We should not "change" our opinions, meaning make old syntax illegal, but we should "level the playing field" by making other forms simpler.

The conclusion of the design document is that we should favor

1. Immutable members in records by default
1. Any features from records that we separate should not make the simple syntax longer

Record feature breakdown

We've also been working on breaking down the individual features of records and determining how independent they can or should be.

Notes: https://github.com/dotnet/csharplang/issues/3137

There seem to be the following somewhat separable parts of records

  1. Value-based equality
  2. Construction boilerplate
  3. Object initializers
  4. Nondestructive mutation
  5. Data-friendly defaults

Value equality

It's been proposed that a key modifier could be applied to signal that value-based equality is being generated based on the members which have it. This works in many cases, but if the absence of the key modifier means inherited equality, we're not sure that's the semantics we want. It would also not allow value-based equality to be "specified" in the base class in some sense, enforcing value equality for the deriving type. Whether this is valuable or blocking is an open question.

Construction boilerplate

Creating a constructor to assign members of a container is one of the largest sources of repetitive boilerplate, e.g.

public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

You can imagine various points on this spectrum to simplify the boilerplate,

public class Point
{
    public key int X { get; }
    public key int Y { get; }
    public Point(X, Y); // name matching and type absence implies initialization
}

Which removes the duplication of naming the same elements multiple times or,

public class Point(X, Y)
{
    public key int X { get; }
    public key int Y { get; }
}

Which removes the constructor name duplication and we could go further to remove property name duplication,

public class Point(
    public key int X { get; }
    public key int Y { get; }
);

Going all the way to the original position deconstruction

public class Point(int X, int Y);

Where we pick a point in this space seems to correspond to the perceived benefits of the orthogonality of the feature. If the construction shorthand is useful for many scenarios outside of the record scenarios, it's practical to expand it.

Object initializers

One benefit to object initializers is that they don't refer to a constructor directly, only to the properties. This sidesteps a weakness in C#, where constructor initialization in inheritance requires repetition. Without constructors the simple relation

public abstract class Person
{
    public string Name { get; }
}
public class Student : Person
{
    public string ID { get;}
}

has no repetition. Each class states only the properties that are essential, and for derived classes all the base properties are inherited without repetition. Once you add constructors this breaks down

public abstract class Person
{
    public string Name { get; }
    public Person(string name)
    {
        Name = name;
    }
}
public class Student : Person
{
    public string ID { get; }
    public Student(string id, string name)
      : base(name)
    {
        Id = id;
    }
}

Now the derived classes have to repeat everything from the base, causing brittleness along the boundary. If we were to imagine some improvement to object initializers, then defining a constructor would not be required.

On the other hand, this also removes one of the main benefits for having a constructor, namely that you can validate the whole state of the object before producing it.