Add design notes

This commit is contained in:
Mads Torgersen 2017-06-01 14:53:15 -07:00
parent c97ea43bc9
commit efe200cf04
2 changed files with 80 additions and 46 deletions

View file

@ -1,14 +1,21 @@
# C# Language Design Notes for Apr 5, 2017
## Agenda
***Raw notes, yet to be cleaned up - read at your own peril***
1. Non-virtual members in interfaces
2. Inferred tuple element names
3. Tuple element names in generic constraints
# Non-virtual members in interfaces
Do we *want* to allow non-virtual declarations in interfaces? In practice you probably at least want private methods. But it's likely you'll want internal, and maybe public. What's the syntactic distinction from normal "virtual/abstract" interface members?:
``` c#
interface I1
{
void M1() { ... } // non-virtual
virtual void M2() { ... }
virtual void M1() { ... }
void M2() { ... } // non-virtual
}
interface I2
{
@ -22,80 +29,100 @@ interface I3
}
```
Do we *want* to allow non-virtual declarations in interfaces? In practice you probably at least want private methods. But it's likely you'll want internal, and maybe public.
If you want this for traits based programming, you'd want to have the same things available, when you "port things over" to a trait.
For traits based programming, you'd want to have the same things available, when you "port things over" to a trait.
Intuitively, though, it feels against the spirit of interfaces.
Intuitively it feels against the spirit of interfaces.
Should an interface be required to put `abstract` in order to allow derived interfaces to override them? No. So it probably also shouldn't require `virtual`. So it's the "virtualness" that's implicit, not specifically the "abstractness".
Should an interface be required to put `abstract` in order to allow derived interfaces to override them? No. So it probably also shouldn't require `virtual`.
The core motivating scenario is default-ness of implementations. So that is the thing we should design around. Is the copy-paste scenario between interfaces and classes important? It already is very far from possible. For instance, property declarations in interfaces today are syntactically identical to auto-properties in classes today!
So it's the "virtualness" that's implicit, not specifically the "abstractness".
Requiring a modifier (e.g. `virtual`) on a virtual interface member with a default member, as in `I1` and `I3` above, causes an inconsistency between interface members with and without default implementations.
The core motivator is the defaultness. So that is the thing we should design around.
Motivators:
- Defaults are useful!
- Java and Swift
- Traits
Is the copy-paste sceario between interfaces and classes important? It already is very far from possible. property decl in interface today is auto-prop in class today!
Also, taking unmodified members with bodies to be non-virtual, as in `I1` above, is a bit dangerous: adding a body changes the member from virtual to non-virtual. While an implementing class may still think it is implicitly implementing the member, it is in fact just declaring an unrelated member:
``` c#
itf I
class C : I1
{
sealed void M() { ... }
void N();
void O() { ... }
}
cls C : I
{
pub void M() => base(I).M();
public void M2() { ... } // Doesn't implement I1.M2, which isn't virtual!
}
```
*Not* requiring a modifier for non-virtual is dangerous: I go add a body, and nothing breaks. No implementors break either, as long as they are implementing implicitly. But then it means something different!
It is better to require a modifier (e.g. `sealed`) on non-virtual members in interfaces, to clearly distinguish them, as in `I2` and `I3` above.
Decision 1: non-virtual should be explicitly expressed through `sealed` or `private`.
Decision 2: `sealed` is the keyword to make interface instance members with bodies non-virtual
Decision 3: We want to allow all modifiers in interfaces
Decision n: Default accessibility for interface members is public, including nested types
Decision n+1: private function members in interfaces are implicitly sealed, and sealed is not permitted on them. Private classes can be sealed, and that means sealed in the class sense of sealed.
The best combination, then, is illustrated by `I2`, where non-virtual members must be marked, and virtual ones must not (and maybe cannot).
Absent a good proposal, partial is still not allowed on interfaces or their members.
Tentatively we'll go forward with the following decisions:
# tuple names
- Non-virtualness should be explicitly expressed through `sealed`
- We want to allow all modifiers in interfaces, in analogy with classes, but with different defaults
- Default accessibility for interface members is public, including for nested types
- `private` function members in interfaces are implicitly `sealed`, and `sealed` is not permitted on them. `private` nested *classes* can be `sealed`, and that means `sealed` in the class sense.
Absent a good proposal, `partial` is still not allowed on interfaces or their members.
# Inferred tuple element names
Anonymous objects allow "projection initializers", where the name of a member can be automatically inferred from an expression ending in a name:
``` c#
new { X = X, Y }; // {X,Y}
new { X = X, Y }; // infers Y as name of second member
```
Projection initializer.
Proposal is to do the same for tuple literals, picking up any names in element expressions if the element doesn't explicitly have a name:
``` c#
int a = 1;
...
var t = (a, b: 2, 3); // (int a, int b, int)
```
Just as with anonymous types we pick up names from expressions that are simple names (`x`), dotted names (`e.x`) and `?.`'ed names (`e?.x`).
This is technically a breaking change from C# 7.0, but it is quite esoteric. It is hard to construct an example where you depend on a tuple element *not* having a particular name, but here's one:
``` c#
Action y = () => {}
var t = (x: x, y) // (int x, Action y) or (int x, Action)?
t.y(); // a call of the second component or of an extension method y?
t.y(); // a call of the second component or of an extension method y on the type (int, int)?
```
We think that this is low enough risk that we can introduce the feature, especially if we do it quickly (C# 7.1).
A subtlety of the feature is that inferred names should not trigger the warnings around unused names:
``` c#
(int, int) t = (x: x, y); // warning for x. No warning for y, because inferred
```
We dropped the feature because we didn't have partially named tuples, but now we do.
Options around breaking change:
# Tuple names in constraints
1. We don't want the feature
2. We want it but it's too late
3. We want it but it has to be now
4. We want it and we don't care when - it's esoteric
There are some gnarly issues questions around tuple element names in generic constraints. In general we try to avoid use of different tuple element names across related declarations, but with constraints it is hard.
We go for 2. Due to the forward breaking change it needs to be tied to a language version. Same rules as anonymous types: picked up from simple names, dotted names and ?.'ed names.
A simple example is
Let's aim for 7.1.
``` c#
class C<U> where U : I<(int a, int b)>, I<(int notA, int notB)> // this is currently allowed, and would become an error
```
## tuple names in constraints
But more complex examples involve indirect differences.
We think you could never declare a class (in C#) that satisfies the constraints (in C#) in the examples. You could construct things in IL to circumvent, but it's ok for us not to deal with that. So it's maybe ok for C# not emit an error here.
``` c#
class C<U> where U : I<(int a, int b)>, I2 // where I2 implements I<(int notA, int notB)>
```
In the case of type implementations, we prevent those and even check for possible type unifications.
But in the case of constraints, we currently dont check as much (no type unification check).
The third level of difficulty is illustrated by:
``` c#
class C<U, T> where U : I<(int a, int b)>, I2<T> // where I2 implements I<(T notA, T notB)>
```
We think you could never declare a class (in C#) that satisfies the constraints (in C#) in these examples. You could construct things in IL to circumvent, but it's ok for us not to deal with that. So it's probably ok for C# not emit an error here.
We do try to emit an error when constraints conflict. But the value is low and it is hard and esoteric and makes the compiler slower.

View file

@ -138,3 +138,10 @@ Design some remaining 7.1 features
1. Nullable scenarios
2. `Span<T>` safety
## Apr 5, 2017
[C# Language Design Notes for Apr 5, 2017](LDM-2017-04-05.md)
1. Non-virtual members in interfaces
2. Inferred tuple element names
3. Tuple element names in generic constraints