Add design notes

This commit is contained in:
Mads Torgersen 2017-05-30 15:56:12 -07:00
parent 7f64711c0a
commit bd45ad6712
6 changed files with 373 additions and 106 deletions

View file

@ -1,14 +1,23 @@
# C# Language Design Notes for Mar 8, 2017
## Agenda
***Raw notes, yet to be cleaned up - read at your own peril***
We looked at default interface member implementations.
1. Xamarin interop scenario
2. Proposal
3. Inheritance from interface to class
4. Overriding and base calls
5. The diamond problem
6. Binary compatibility
7. Other semantic challenges
## Xamarin
# Xamarin interop scenario
Interop with Java. Won't be able to surface or implement Java interfaces in C# anymore. There might be Android interfaces we can't implement.
Android interfaces are written in Java, and can therefore now have default implementations on members. Xamarin won't be able to seamlessly project those interfaces into .NET.
On iOS, they have protocols
On iOS, Objective-C and Swift have protocols, of the general shape:
``` c#
protocol Foo
@ -20,7 +29,7 @@ protocol Foo
}
```
Maps to C# interface
Again, the best we can do is to project the non-optional parts into C# interfaces, whereas the optional parts must be handled in less appetizing ways:
``` c#
interface IFoo
@ -29,20 +38,19 @@ interface IFoo
}
```
(lots more complicated stuff)
We currently have a Swift importer, and that looks horrible.
# Proposal
Change: allow the following in interfaces
[The current proposal](https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md) is to allow the following in interfaces:
- member bodies
- static and private methods
- override methods
- nonvirtual static and private members
- overrides of members
Not proposing:
- non-virtual public members
Putting a concrete body on your method (etc), means you don't have to implement it on the class.
Not proposing (but may want to look at):
- non-virtual public instance members
- protected and internal members
- nested types
- operator declarations
@ -52,107 +60,91 @@ Don't want:
- state!
- conversions
This gives parity with where Java is: they made tweaks over time based on feedback, and this is where they landed (private and static were added).
This gives parity with where Java is: they made tweaks over time based on feedback, and this is where they landed (private and static members were added).
The more you go to the side of adding things, the more this is a philosophy change for interfaces.
Should overrides be able to call `base`? Yes, but would need new syntax, probably. Diamond problem!
Putting a concrete body on your method (etc), means you don't have to implement it on the class.
# Inheritance from interface to class
Important question: is the member inherited into the class, or is it "explicitly implemented"? Our assumption is that it's explicitly implemented. That means you can't "just" refactor similar implementations into an interface the way you do a base class.
Possible syntax for base calls: `I.base.M()`.
Important question: is the member inherited into the class, or is it "explicitly implemented"? Our assumption is that it's explicitly implemented:
``` c#
itf IBase
{
void M() {}
}
itf IDerived : IBase
{
override void M() {} // or
override void I.M() {} // to choose one
}
interface I { int M() => 0; }
class C : I { } // C does not have a public member "M"
```
The diamond problem:
That means you can't "just" refactor similar implementations into an interface the way you do into a base class.
# Overriding and base calls
Should overrides be able to call `base`? Yes, but we would need new syntax to deal with ambiguity arising from the fact that you can have more than one base interface:
``` c#
itf IA { int M() => 1; }
itf IB : IA { int M() => 2; }
itf IC : IA { int M() => 3; }
itf ID : IB, IC{} // fine, or a warning, but
cls C : ID {} // must override to choose
interface I1 { void M() { WriteLine("1"); } }
interface I2 : I1 { override void M() { WriteLine("2"); } }
interface I3 : I1 { override void M() { WriteLine("3"); } }
interface I4 : I2, I3 { override void M() { base(I2).M(); } }
```
A common example may be `IDisposable`, and also the `Reset` method of `IEnumerable`.
For base, it may be that you specifically designate one that has a concrete implementation.
Base syntaxes:
The exact syntax for disambiguating base calls is TBD. Some ideas:
``` c#
I.base.M()
base(I).M()
base.I.M() // ambiguous with existing meaning
base<I>.M() //
I.base.M() // what Java has
base(I).M() // similar to default(I)
base.I.M() // ambiguous with existing meaning
base<I>.M() // looks like generics
```
## Binary compat
# The diamond problem
What does the runtime do when an interface adds a default impl and that causes an ambiguity in code that isn't recompiled?
`I4` above inherits two default implementations, one each from `I2` and `I3`, but explicitly provides a new implementation, so that there's never a doubt as to which one applies. However, imagine:
Should this be a runtime error? It would lead to some hard-to-understand failures. Should it "pick one"? That's what Java does, in a deterministic way. Order matters. We have similar issues with variance today, and there we just pick one.
``` c#
interface I5: I2, I3 { }
class C : I2, I 3 { }
```
The class declaration of `C` above must certainly be an error, since there is no good (symmetric) unambiguous way of determining a default implementation. Also, the interface declaration of `I5` should be an error, or at least a warning.
This means that adding an override to an interface can break existing code. This can happen in source, but depending on compilation order, it may also be possible to successfully compile such code.
# Binary compatibility
What should the runtime do when an interface adds a default implementation and that causes an ambiguity in code that isn't recompiled?
Should this be a runtime error? It would lead to some hard-to-understand failures. Should it "just pick one"?
"Why did you not let my program run?" vs "why did you not prevent this hole"?
Let's err on the side of the former: let the program run.
We need to decide what is less harmful. We should look at what Java does and why.
# Other semantic challenges
# Some semantic challenges
The feature reveals some "seams" between C# and the CLR in how they understand interface overriding. In the following, imagine `I`, `C` and the consuming code are in three different assemblies:
``` c#
// step 1
itf I { }
interface I { }
cls C : I { public void M(); }
class C : I { public void M() { /* C.M */ } }
// step 2
itf I { void M() { /* 1 */} }
interface I { void M() { /* I.M */} }
I i = new C();
i.M(); // does default impl
i.M(); // calls I.M default implementation
// step 3: recompile C
I i = new C();
i.M(); // calls C.M();
i.M(); // calls C.M non-virtual method
```
This is caused by an impedence mismatch btw C# and the runtime. We need to decide if there's something we can do
### 2
If a class has a member with a signature that matches a member of an implemented interface, we require the rest of the method (e.g., return type) to match. Should the presence of a default implementation change that? No.
## Breaks
Should we as a fw team consider these only "theoretical breaks", and allow the fw to add default implementations? Otherwise the value isn't enough.
Java may also have the problem that adding an override in an interface *after the fact* is a breaking change.
## Open questions
Implementation
covariant returns
The problem here is that the runtime doesn't consider non-virtual members able to implement interface members, but C# does. To bridge the gap, C# generates a hidden virtual stub method to implement the interface member. However, during step 1 there is no interface member to implement, and during step 2 the class declaration isn't recompiled. The runtime doesn't consider `C.M` an implementation of `I.M`, so if you call `M` on a `C` through the `I` interface, you get the default behavior. As soon as `C` is recompiled, however, the compiler inserts its stub, and the behavior changes.
We have to decide it there is something we can and will do about this. We may just accept it as a wart.

View file

@ -1,23 +1,35 @@
# C# Language Design Notes for Mar 15, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
*Quote of the Day:* "Little leek - isn't that called a spring onion?"
*Quote of the Other Day:* "Who's champion for recursive patterns? It says 'see champion for recursive patterns'."
## Agenda
Triage of championed features
1. JSON literals
2. Fixing of captured locals
3. Allow shadowing of parameters
4. Weak delegates
5. Protocols/duck typing/concepts/type classes
6. Zero and one element tuples
7. Deconstruction in lambda parameters
8. Private protected
Triage
## JSON literals
# JSON literals
No
## Fixing captured locals
Not worth any effort, probably even up for grabs
# Fixing of captured locals
## Allow shadowing of parameters
Not worth any effort, probably even as up for grabs
Current rule saves your bacon a couple of times
# Allow shadowing of parameters
Current rule saves your bacon a couple of times:
``` c#
var t = FooAsync(...)
@ -29,32 +41,27 @@ Discards would make this worse, in a sense, because they lead people to be more
Shadowing could be used to avoid capturing, but that would be better served by a non-capture feature. Could be a `static` on lambdas to avoid capture - or it could be a capture list approach.
"Capture nothing", "capture what I want", "capture this" are three levels with different cost.
"Capture nothing", "capture what I want", "capture this" are three potential levels with different cost.
Could be `this`, `static` and nothing in front of the lambda. But it might be nice to have an explicit annotation for the default, for people who care to express that they explicitly thought about it.
Individual items is worthless if you don't care about different *kinds* of capture for a given variable.
Individual items in a capture list are worthless if you don't care about different *kinds* of capture for a given variable.
Also a great candidate for custom annotations so we don't put it in the language. That would also allow folks that want more detail to have that.
Attributes on lambdas seem like a more general feature. Let's feed this in as a scenario for attributes on lambdas.
## Weak delegate
# Weak delegate
Best approach is the small leak pattern which creates a wrapper delegate with a weak reference to the original receiver. That doesn't solve the whole problem, though - how about unregistrering, etc?
Best approach is the small leak pattern which creates a wrapper delegate with a weak reference to the original receiver. That doesn't solve the whole problem, though - how about unregistering, etc?
"Little leek - isn't that called a spring onion?"
This isn't necessarily a language feature. This should be a library that you pass the method to, and it wraps it. It would be helped by a delegate constraint.
This isn't necessarily a language feature. This should be a library that you pass the method to, and it wraps it. It would be helped by the langauge allowing for a delegate constraint.
Conclusion: Not as a language feature right now; figure it out as a library feature, and *maybe*, someday we would add syntax around it.
``` c#
```
## Protocols/duck typing/concepts/type classes
# Protocols/duck typing/concepts/type classes
More dynamic or more static approaches to "adding a 'type' after the fact".
@ -62,9 +69,10 @@ A separate important subissue is the dependency explosion.
Different solutions to the same problem. They should be addressed as one design effort.
8.0 is crazy, but helps us prioritize investigation work
8.0 as a milestone is probably crazy and unrealistic, but helps us prioritize investigation work
## 0 and 1 element tuples
# Zero and one element tuples
``` c#
var () = e; // who cares to deconstruct?
@ -72,15 +80,15 @@ var (x) = e; // just get through property?
var (x, (y)) = e; // recursive - can't just grab a member
```
In other languages `()` are list-like unit type things.
In other languages `()` are list-like unit type things - an alternative to `void`, that is more of a first class type and can therefore be used in generics.
``` c#
() M(...) => (); // returns zero things
```
Also if we ever go to bool-returning Deconstruct methods (or the like), you might want zero-length decosntructors to mean "there wasn't anything".
Also if we ever embrace bool-returning Deconstruct methods (or the like), you might want zero-length decontructors to mean "there wasn't anything".
Womples are more likely to be useful, especially for deconstruction. But they are also the ones that clash most fiercely with existing syntax (parenthesized expressions!).
Womples (one-element tuples) are more likely to be useful, especially for deconstruction. But they are also the ones that clash most fiercely with existing syntax (parenthesized expressions!).
``` c#
() t = ();
@ -116,11 +124,10 @@ if (o is OneDPoint(3)) ...
if (odp is (3)) ...
```
Resolve this with
Open questions abound.
"Who's champion for recursive patterns? It says "see champion for recursive patterns".
## Deconstruction in lambda parameters
# Deconstruction in lambda parameters
It's a special case of deconstruction in parameters
@ -129,7 +136,7 @@ It's a special case of deconstruction in parameters
double M((int x1, int y1), (int x2, int y2)) => Sqrt(x1 * x2 + y1 * y2);
```
## private protected
# private protected
Would not use so much in the BCL, but in APIs with deeper hierarchies, definitely.

View file

@ -0,0 +1,55 @@
# C# Language Design Notes for May 16, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
# 7.1
## Pri pro almost there, push to 7.2
## #42 Field-targeted attributes
Community contributed? If so push to 7.2
# 7.2
"Slicing" needs to be split into "ref structs" and "slicing syntax"
Push out everything, except things that are "in theme", and things that are almost done (field-targeted, private protected)
Keep everything that's in theme until we can sit with the Span folks and prioritize.
# GitHub
Issues as a notification mechanism: start using that for design notes and proposals. Revisions are comments on the same issue.
# Triage of championed features
CallerArgumentExpression, push to 7.X and start process for attribute
0b_ 0x_
Native ints
Static delegates: They require, generally, 2 allocations, are crappy for interop. Static delegates are typed `IntPtr`s, essentially.
`ValueAction` and `ValueFunc`. Lot of asks from CoreRT, for PInvoke etc.
## XML doc comments
This is hard to take as a community contribution: it's primarily about the IDE behavior. Not all of that may even be open source today.
It would also affect ecosystem, which would have to handle it. Need to coordinate with IDE team.
## `??` and `?.` for pointers
Probably lo-pri to make this pleasant. But nothing against it. This seems suitable for up-for-grabs
It should probably be `?->`. Double pointers, you're out of options.
## Non-trailing named arguments
Helps selectively put names, and therefore people don't bend over backwards to put name-prone parameters last. Also would make it work better with params.
Would need some ide work.

View file

@ -0,0 +1,109 @@
# C# Language Design Notes for May 17, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
# Conflicting override
If at runtimes you have ambiguous overrides
``` c#
interface I1 { void M1(); }
interface I2 : I1 { override void M1() { ... }}
interface I3 : I1 { override void M1() { ... } }
class C : I2, I3 {} // what happens when you load?
C x;
x.M1(); // what happens when you call?
```
This can happen through certain orders of compilation.
Options:
1. throw on load
2. throw on call (like Java)
3. pick arbitrarily (but consistently)
4. bake in what you meant at compile time
1. always use the baked in, error if it's gone
2. pick the unique one, fallback to baked in if unsuccessful
Baking in causes a lot to hinge on recompilation. We should leave to the runtime to resolve the virtual call. So not 4. Also, 1 seems too harsh. If you don't call it, why complain? If you call an arbitrary method, that's a little dangerous: you can't reason about what the program does.
Option 2 it is. We can throw. If we realize we were wrong about this later, we can move from there. No bake in.
## Is the overriding logic handled at compile time or runtime
Yes
# Does the compiler encode what the most specific override is
No
## interaction with variance
# Can Main be in an interface?
Who cares? Aleksey does. Yes. Because that's the least work.
# Static constructors in interfaces?
Probably, but let the runtime folks think about this. Reason is static fields. We allow initializers for static fields, but not an explicit static constructor. Can think about adding later.
# In classes you can define virtual prop with private accessor. Allowed in interfaces? What does it mean?
# Does an override introduce a member
- Does it introduce parameter names
- Does it introduce a new virtual method that could be implemented independently
If the call site sees two separately declared methods, that's already an ambiguity.
``` c#
interface I1 { void M1(); }
interface I2 : { void M1() { ... } }
interface I3 : I1, I2 { override void M1() { ... } }
I3 x;
x.M1();
```
Should I3 be allowed?
Should x.M1();
Explicit override syntax removes the problem, but do we want to force people to always put the interface in front.
Java's solution does not help us, because independently declared interface members unify.
Xamarin does not expect to need override. They'll just need to know whether there is a default implementation or not. They will need reflection support to query the default methods.
It's not an option to depend on order.
Resolution: I3 is fine, but does not introduce a new member. So the call to M1 is ambiguous. The work-around is to cast to one of the interfaces. However, oddly, you *can* call it through base(I3).
# Parameter names
``` c#
interface I1 { void M1(int a); }
interface I2 : I1 { override void M1(int b); } // allowed to change parameter names?
I2 x;
x.M(a: 3); // allowed?
x.M(b: 4); // allowed?
```
The class behavior is to pick the most specific names, but that does mean that introducing an override (with different parameter names) can be a breaking change. But we could issue guidance to not change names in this situation.
From experience, people do intentionally change names on override, but others ask for features to prevent them from doing it!
Common scneario is implementing a generic instance. Going from "T" to Dog.
Let's take the names from the original declaration, deliberately being inconsistent with the class rule for simplicity's sake.
# New open question:
what about base(I2) and parameter names?

View file

@ -0,0 +1,77 @@
# C# Language Design Notes for May 26, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
## Agenda
- Native ints
# Native ints
1. Improve `IntPtr` with operators
2. Add `nint` and `nuint` as a user defined struct (the Xamarin approach)
3. Add `nint` and `nuint` to the language
1. compile down to native ints and add an attribute to persist in metadata (doesn't solve `ToString`)
2. project language `nint` etc to user defined structs
4. A combo of 1 and 3
State of the world: We have `IntPtr` which has crappy arithmetics, and enough use that it would be breaking to change anything.
Xamarin need to do interop, so they have a user defined struct.
`IntPtr` has a few operators: `+` with `int`, which is checked on 32 bit and unchecked on 64 bit! And some conversions. Plus a very unfortunate `ToString` implementation.
3.2 looks like this:
``` c#
struct NativeInt
{
public IntPtr Value;
public override string ToString() { ... }
}
/// etc
```
But operators are implemented by the language, not as UD operators.
Diff between 3.1 and 3.2 is that with 3.2 at runtime we have different types, so we can have differentiated runtime behavior: `ToString` and reflection.
A downside is that operations that take `ref IntPtr` (like Interlocked...) wouldn't automatically take `ref nint`. Having the public mutable field would let things still work for people, and we could go through and add `nint` overloads over time to make it better.
This should be in corlib. Is there anything we can do to mitigate in the meantime? We could ship a nuget package etc, but there's some cost to that, including indefinite maintenance. But some of the people who would benefit from this will be in a terrible spot if we don't provide something.
We also need to deal with native float. There is no option to do 3.1 for floats; there is no `IntPtr` equivalent. So that one would need a framework type. However, we could probably live with that nfloat struct moving into the frameworks - other than Xamarin, which would add it faster for its interop scenarios.
With 3.1, if you consume a `nint`-attributed `IntPtr` with an old compiler, would it treat it as an `intPtr`? If that's the case then the code would subtly change behavior on compiler upgrade. Unfortunate! We could poison `nint` with `ModReq` so that they cannot be consumed by existing compilers, but now `nint` really *is* a different type, and requires separate overloads of methods that take it as a parameter.
Another option is to obsolete the UD operators on `IntPtr`, to drive people to use `nint` instead.
Objections to 3.2:
- Adoption, where a separate struct would take a while to propagate (we feel we've mostly mitigated this)
- We'll emit slightly less efficient and more verbose IL in a couple of cases
- Needing new overloads for `nint` where there are `IntPtr` overloads today
Objection to 3.1:
- No runtime distinction (reflection and `ToString`)
- `ToString` happens all the time
Who has the benefit
- Identity conversion: 3.1
- ToString: 3.2
- IL efficiency: 3.1
- Preserve the type on boxing: 3.2
- Adoption cost: 3.1, but only slightly
- Can overload: 3.2
- Does not need to overload: 3.1
We're balanced on preferring 3.1 vs 3.2. Could maybe be convinced to 3.2 if we can solve how do existing users of `IntPtr` migrate.

View file

@ -68,7 +68,7 @@ We went over the proposal for `ref readonly`: [Champion "Readonly ref"](https://
1. Shapes and extensions (*exploration*)
2. Conditional refs (*original design adopted*)
# Mar 7, 2017
## Mar 7, 2017
[C# Language Design Notes for Mar 7, 2017](LDM-2017-03-07.md)
We continued to flesh out the designs for features currently considered for C# 7.1.
@ -76,3 +76,30 @@ We continued to flesh out the designs for features currently considered for C# 7
1. Default expressions (*design adopted*)
2. Field target on auto-properties (*yes*)
3. private protected (*yes, if things work as expected*)
## Mar 8, 2017
[C# Language Design Notes for Mar 8, 2017](LDM-2017-03-08.md)
We looked at default interface member implementations.
1. Xamarin interop scenario
2. Proposal
3. Inheritance from interface to class
4. Overriding and base calls
5. The diamond problem
6. Binary compatibility
7. Other semantic challenges
## Mar 15, 2017
[C# Language Design Notes for Mar 8, 2017](LDM-2017-03-15.md)
Triage of championed features
1. JSON literals
2. Fixing of captured locals
3. Allow shadowing of parameters
4. Weak delegates
5. Protocols/duck typing/concepts/type classes
6. Zero and one element tuples
7. Deconstruction in lambda parameters
8. Private protected