add and edit design notes

This commit is contained in:
Mads Torgersen 2018-05-18 16:04:30 -07:00
parent f9cf91154d
commit ba3a4cd71b
4 changed files with 252 additions and 25 deletions

View file

@ -1,5 +1,7 @@
# C# Language Design Notes for Apr 25, 2018
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
## Agenda
@ -139,9 +141,9 @@ T M<T>()
}
```
This could be an argument for `T?` that is not about special methods. Or it is an argument against having the W warnings at all.
This could be an argument for allowing `T?` that is not about special methods. Or it is an argument against having the W warnings at all.
Let's keep this example around and revisit. But for now, let's consider `default(T) to be potentially null, and therefore warn on its unguarded use.
Let's keep this example around and revisit. But for now, let's consider `default(T)` to be potentially null, and therefore warn on its unguarded use.
# Annotations

View file

@ -1,5 +1,7 @@
# C# Language Design Notes for Apr 30, 2018
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
# Switch expressions
@ -15,7 +17,7 @@ e switch
}
```
No default: use `_`.
No `default` - instead use `_`.
If the compiler thinks there are cases you don't handle, it'll warn. If you actually don't handle a case we throw an exception (NRE for prototype, something else in the long run).
@ -32,17 +34,25 @@ The lack of statements inside may be a frustration, but that's orthogonal. Let's
## Exploration
Is there a way to generalize the syntax from the conditional (used to be ternary!) operator?
Is there a way to instead generalize the syntax from the conditional (ternary) operator? After all, semantically speaking this could be viewed as a general form: Conditional operators and switch expressions on bool are semantically equivalent.
``` c#
e
? true => e1
: false => e2
```
The fact that you know the number of colons today means you can have fewer parentheses than you would get away with here.
``` c#
e ?
true => e1 ? x : y :
false => e2
e
? true => e1
: false => e2
```
It's not in fact obvious whether there should be many `?`s and one `:`, or one `?` and many `:`s:
``` c#
// Interpret ? as following the tested expression, and : as a separator of test/result pairs
e
? 1 => "one"
@ -56,15 +66,21 @@ e
? 2 => "two"
? var x when x > 2 => "too many"
: "too few"
```
The `=>` glyph is probably not right in a `?:` style syntax. It would have to be something else that more clearly signals pattern/result pairs.
``` c#
e
? 1 -> "one"
? 2 -> "two"
? var x when x > 2 -> "too many"
: "too few"
```
## Considering our options
``` c#
// Compromise - terser
e ? { 1: "one", 2: "two", var x when x > 2: "two many", "too few" }
@ -128,9 +144,10 @@ strings.Select(x => x switch { null => 0, _ => x.Length }); // Lots of => with
Argument against `->`: Has meaning in unsafe code
Argument against `:` as used in 4: Clashes with other uses of `:`. But
Argument against `:` as used in 4: Clashes with other uses of `:`.
Where input is an expression rather than a variable:
``` c#
M(e switch { null => 0, var x => x.Length }); // 1 - 0
M(e switch { null: 0, var x: x.Length }); // 2 - 13 - 6
@ -143,19 +160,9 @@ M(e ? { null -> 0, var x -> x.Length }); // 6 - 0
We might want to allow the last thing to be a default value without pattern, but not in the prototype.
So the prototype will have version 2.
Conditional operators and switch expressions on bool are semantically equivalent.
The fact that you know the number of colons today means you can have fewer parentheses than you would get away with here.
The `=>` is probably not right in a `?:` style syntax. It would have to be something else that more clearly signals pattern/result pairs.
## Conclusion
The prototype will have version 2. We're saving for later whether the last clause should be able to leave off a pattern.
# Property pattern
@ -167,7 +174,7 @@ if (e is { Name == "Mads", Employer == { ID == string id } }) { WriteLine(id); }
if (e is { Name is "Mads", Employer is { ID is string id } }) { WriteLine(id); } // 4
```
1 is what we have implemented, but clashes a little with what we just decided for switch expressions.
1 is what we have implemented, but it clashes a little with what we just decided for switch expressions.
2 mirrors object initializers the most
3 implies equality, but clashes in meaning with `==` elsewhere
4 emphasizes `is` as a means for applying patterns
@ -191,5 +198,5 @@ var result = person switch
_: null
};
```
## Conclusion
Decision: stay with `:` for prototype, remains open question though!

View file

@ -0,0 +1,98 @@
# C# Language Design Notes for May 2, 2018
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
# Revisit syntax for switch expression
We got a lot of feedback on the decision to use `:` between pattern and result in each case of a switch expression, and we want to revisit it one more time before releasing the prototype.
``` c#
state = (state, action) switch {
(DoorState.Closed, Action.Open) => DoorState.Opened,
(DoorState.Opened, Action.Close) => DoorState.Closed,
(DoorState.Closed, Action.Lock) => DoorState.Locked,
(DoorState.Locked, Action.Unlock) => DoorState.Closed,
_ => state
};
state = (state, action) switch {
(DoorState.Closed, Action.Open) -> DoorState.Opened,
(DoorState.Opened, Action.Close) -> DoorState.Closed,
(DoorState.Closed, Action.Lock) -> DoorState.Locked,
(DoorState.Locked, Action.Unlock) -> DoorState.Closed,
_ -> state
};
state = (state, action) switch {
(DoorState.Closed, Action.Open) ~> DoorState.Opened,
(DoorState.Opened, Action.Close) ~> DoorState.Closed,
(DoorState.Closed, Action.Lock) ~> DoorState.Locked,
(DoorState.Locked, Action.Unlock) ~> DoorState.Closed,
_ ~> state
};
state = (state, action) switch {
(DoorState.Closed, Action.Open) : DoorState.Opened,
(DoorState.Opened, Action.Close) : DoorState.Closed,
(DoorState.Closed, Action.Lock) : DoorState.Locked,
(DoorState.Locked, Action.Unlock) : DoorState.Closed,
_ : state
};
```
There are pros and cons for all of these. In our previous decision we may have overemphasized one set of examples over another.
`:`: Has an affinity to `switch` statements - it just removes the `case`. However, in `switch` statements, *statements* come after, whereas here, *expressions* do.
`=>`: We have already put those elsewhere (expression bodied members) to say that you yield an expression as a result. However:
1. Pain to do parsing magic for the last one, maybe. (Not a strong argument)
2. precedence issues: if you had a `?:` in your pattern or `when` clause it would consume `=>` for a lambda
3. Looks like a lambda but isn't one
4. Will drive an expectation of a block body
Conclusion: We're undecided, but we will switch back to `=>` for the prototype.
# Nullable special members
``` c#
static bool IsNullOrEmpty([NotNullWhenFalse] string? s) { }
```
Should use "When" in the name.
``` c#
static void AssertNotNull<T>([EnsuresNotNull] T? t) where T : class { }
```
Not necessarily for fatal methods. Could be a method that initializes a ref:
``` c#
static void EnsureNotNull([EnsuresNotNull] ref string? s) { if (s is null) s = ""; }
```
Probably needs "Ensures" here to signal difference from restriction on incoming. Maybe "Ensures" on all of them?
``` c#
class Object
{
[NullableEquals] public static bool ReferenceEquals(object? x, object? y) { }
[NullableEquals] public static bool Equals(object? x, object? y) { }
[NullableEquals] public virtual bool Equals(object? other) { }
[NullableEquals] public static bool operator==(object? x, object? y) { }
}
```
For operators and static binary methods on Object, the compiler can just know. For other user defined equalities we need an attribute. Also `IEqualityComparer` and `IStructuralEqualityComparer`, both generic and non. We probably still want to put the attribute on binary equality methods for clarity, but the behavior will be there regardless for the ones the compiler knows about.
For unary instance method equalities, the attribute should just be `[NotNullWhenTrue]` (since the receiver is inherently not null!). Again, for the ones the compiler knows about (`Object.Equals` and `IEquatable.Equals`), we should have the behavior regardless of the attribute, but we should probably put the attribute in anyway.
For *not* equal methods we may not need an attribute, since non-operator not-equalses are probably exceedingly rare.
We're a little concerned about int-returning comparers, since chasing in a compiler analysis whether their result is 0 is quite subtle. We're open to discussing it again.
As for `[NullableEquals]` on bool-returning binary equality comparisons, we should make it only work for exactly two parameters for now. If there are more parameters, it's not going to be clear that the first two are the ones being compared.
`[NullEquality]` for now.
We think we know what it does but we should drill in later to be sure.
For the virtual method, first there is the concern about whether we can expect overrides to follow the contracts.
Aside: when we see `o.x` with an instance member, then we should know that after, o was not null. In general, should we consider scoping reasoning around throws to the nearest catch clause?

View file

@ -0,0 +1,120 @@
# C# Language Design Notes for May 14, 2018
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
## Agenda
We discussed reactions and feedback from talk and booth at the BUILD conference.
# Nullable
## Special methods
We're on the right track
## External annotations
We probably need them, we'll get back to it
## Incoming values to public API
The feature doesn't help people remember to check non-null parameters for null. We could make it so that the null state of such parameters starts out as MaybeNull.
This would definitely help people follow the current practice of aggressively protecting against unwanted null parameters. If in the future callers become more "reliable" on average, due to widespread adoption of the feature, then this may seem too harsh.
Of course this wouldn't help when somebody passes you a `string[]` with nulls in it. And unless the parameter is actually dereferenced (or passed as non-nullable) inside of the method, you'd still get no warning. This speaks to maybe having a separate analyzer to help you remember to check, instead of building it into the nullable feature?
"Public" could be `public`, `protected` and `protected internal`, or could be more dialable. Oftentimes people use public without intending it to be an API. They may be forced to by some other circumstance: interface implementation, or databinding etc.
It's also a problem if multiple overloads leave the checking to just one of them, that they all delegate to. Now they will get warnings when they just pass on the parameter.
There are lots of people who do not do argument checking, but instead leave the parameter to NRE on first occasion. This would work against that pattern.
This rule feel somewhat more ad hoc than the other nullability rules, especially as it depends on accessibility.
### Conclusion
Definitely worth discussing the scenario. There are many counterexamples to doing it like this, but it also seems reasonable to help people who want to do argument validation remember to do so. We're leaning to where this is an analyzer, not part of the feature itself.
# Index and Range
Generally the approach was approved of. It deals with some of the limitations in Python, for instance. The full generality of index expressions (working outside of indexing, having a natural type) wasn't always completely appreciated, but also wasn't considered harmful. The `^` glyph is a bit foreign at first, but nothing worse than that.
## Conclusion
We're on the right track.
# Default interface member implementations
Some reaction against it, where it just seems really wrong to put code inside of an interface. Also, many people don't have the problem of evolving interfaces: if those interfaces are only implemented in their own code bases, they can just fix things up. So the motivation doesn't really apply to them.
This is important if you are building public API, and also for consuming API from other languages, with Java and Swift interop in Xamarin being a prime example.
## Conclusion
We still think this is an important feature.
# Records and discriminated unions
The main motivation for people who ask for these features seems to be simply conciseness. Exhaustiveness checking is interesting, but not nearly as important.
Exhaustiveness is in fact not obvious:
``` c#
sealed class Animal
{
sealed class Dog: Animal {}
sealed class Cat: Animal {}
}
int M(Animal a)
{
return a switch
{
Cat c => 1,
Dog d => 2,
}
}
int M(Box<Animal> b)
{
return b switch
{
Box(Cat c) => 1,
Box(Dog d) => 2,
}
}
```
These two switches may look exhaustive, but are actually missing null cases both of the `Box` and the `Animal`. To be exhaustive they should really be:
``` c#
int M(Animal a)
{
return a switch
{
Cat c => 1,
Dog d => 2,
null => 3
}
}
int M(Box<Animal> b)
{
return b switch
{
Box(Cat c) => 1,
Box(Dog d) => 2,
Box(null) => 3,
null => 3
}
}
```
We probably want to have different exhaustiveness checking depending on whether `a` is `Animal` or `Animal?`. But that means that missing null cases can only yield warnings, just like nullability does, since we don't want nullability to have any semantic effects or yield errors.
## Struct unions
We could work with the runtime to allow us to overlay different types inside of a struct. For unions where each variant is small, structs would probably be highly preferable from a performance perspective.