84 lines
No EOL
6.4 KiB
Markdown
84 lines
No EOL
6.4 KiB
Markdown
# C# Language Design Notes for Mar 1, 2017
|
|
|
|
## Agenda
|
|
|
|
1. Shapes and extensions (*exploration*)
|
|
2. Conditional refs (*original design adopted*)
|
|
|
|
|
|
# Shapes and extensions
|
|
[Exploration: Shapes and Extensions](https://github.com/dotnet/csharplang/issues/164)
|
|
|
|
The write-up explores a version of "type classes" for C#, that can be implemented by types by means of extension declarations. It is aimed at improving the ability to adapt code, and to abstract over shapes of types without having to predict the need when the types are declared.
|
|
|
|
This is static approach, where you imbue types with new members *statically*, but they are not part of the objects at runtime. This can be contrasted with runtime ideas of
|
|
|
|
* "protocols", understood as real, runtime types that objects can fit "after the fact"
|
|
* "extension interfaces", where a type can be made to implement an interface in separate code
|
|
|
|
Both of these lead to objects having new types at runtime, whereas type classes - the present proposal - create a compile time "illusion", which leads to certain different trade-offs. These are competing approaches to attain many of the same goals, and in the end we'd have to weigh the trade-offs and decide one approach to go with. It's possible that we could devise a middle ground between the more static and the more dynamic approaches.
|
|
|
|
## Implementation
|
|
|
|
One such trade-off is whether the runtime needs to change. The type-classes proposal comes with an efficient implementation strategy on top of the existing runtime.
|
|
|
|
The implementation exploits generic specialization for value types. This leads to many instantiations of the same generic type at runtime, which usually isn't too much of a concern from a memory perspective, but does add significantly to startup cost. Precompilation mechanisms such as NGEN can help with this, but those vary between platforms.
|
|
|
|
The proposal is assuming no changes to the runtime, but you could make the runtime do better and understand more, even in compatible ways. This is worth exploring.
|
|
|
|
## Impedance mismatch
|
|
|
|
One downside of a static approach is that it relies on translation into different runtime concepts. Therefore the language-level compile-time world ends up differing more from the run-time world as seen through reflection and from other languages. And we have to remember that such "other languages" include existing versions of C#, unless the feature implementation does something to specifically "poison" the code for down-level consumption.
|
|
|
|
It's worth calling out, for instance, that many approaches to dependency injection rely on selecting between interface implementations through reflection. This would probably not work well with the "shapes" of the proposal, as the "implements" relationship is no longer evident at runtime.
|
|
|
|
This particular proposal goes further in hiding the details of the translation than e.g. [the proposal](https://github.com/MattWindsor91/roslyn/blob/master/concepts/docs/csconcepts.md) it is inspired by. While that leads to a simpler user-level model perhaps, it also hides details that are important at the next level down (such as extra type parameters!), and which would need to be reconciled with older versions of the C# compiler encountering this in libraries, as well as with other languages.
|
|
|
|
A runtime approach would be able to keep a faithful representation of the language-level world. This would let typechecks and reflection "get it right", and counteract the language and runtime gliding further away from each other. However, it would lead to challenges interacting with existing code: If new concepts are introduced at the assembly level, older compilers are likely to be confounded, and effectively prevented from consuming it.
|
|
|
|
## Issues with the specific approach
|
|
|
|
The proposal uses "extension declarations", as already envisioned through ["extension everything"](https://github.com/dotnet/roslyn/issues/11159) proposals, to also "witness" a type implementing a shape. This is intended to lower the concept count and unify some related ideas, but it also can be seen as too much of a conflation.
|
|
|
|
The disambiguation scheme in the proposal will not work when type parameters are constrained by more than one shape, each of which needs to be witnessed.
|
|
|
|
We should make sure we think through shapes that have more than one type parameter.
|
|
|
|
## Conclusion
|
|
|
|
We should keep talking about ways to "add types to objects", and in particular try to do a similar exploration of more runtime-based approaches, so that we can compare.
|
|
|
|
|
|
# ref conditional operator
|
|
|
|
In the [previous meeting](LDM-2017-02-28.md) we left open the syntax for the conditional operator over variables:
|
|
|
|
``` c#
|
|
cond ? ref v1 : ref v2 // original proposal
|
|
cond ? v1 : v2 // alternative: just figure out if it's variable or not
|
|
```
|
|
|
|
The alternative was considered on account of being more succinct. However, it leads to problems. On the semantic level:
|
|
|
|
``` c#
|
|
(cond ? v1 : v2).M(...); // breaking change?
|
|
```
|
|
|
|
What if `v1` and `v2` are of a value type, and `M` is a mutating method? Today `M` mutates a copy, which is probably silly and a bug. But it's been harmless for a long time, and there may be people depending on it.
|
|
|
|
- If we change the behavior to mutate the selected variable, then that's a breaking change
|
|
- If we don't, then you cannot as easily express mutating the original variable when it is *actually* useful
|
|
|
|
There's also a more implementation-oriented issue:
|
|
|
|
``` c#
|
|
(cond ? v1 : v2).M(await ...); // can't "spill"
|
|
```
|
|
|
|
When generating code for an await, we need to "spill" the stack, including any variable refs sitting on it, to the heap. Since refs cannot be in the heap, we do tricks: since we know the variable may need spilling, we don't actually resolve it in place. Instead we keep around the constituent parts (e.g. an array and an index, an object and a member, etc) and only look it up *after* the code has resumed after the await.
|
|
|
|
But here we cannot easily store the constituent parts, because we won't know which branch of the conditional will end up getting executed, and each may lead to different forms of constituent parts! While there may be extremely sneaky and possibly expensive ways around this, we could also just forbid the await. However, the error message would be very strange indeed if it came on an ordinary shape of conditional, whereas it is easier to say referring to a special syntactic form of the conditional.
|
|
|
|
## Conclusion
|
|
|
|
On the whole, these challenges lead us to prefer the original proposal with the explicit occurrences of `ref` in the syntax. |