From 706a455a57b1d4f44dbda978229343af9fd3e6af Mon Sep 17 00:00:00 2001 From: Neal Gafter Date: Mon, 30 Jan 2017 14:35:20 -0800 Subject: [PATCH] Add LDM meeting notes from 2015 onward. (#7) * Add LDM meeting notes from 2015 onward. --- meetings/LDM-2015-01-21.md | 434 ++++++++++++++++++ meetings/LDM-2015-01-28.md | 297 ++++++++++++ meetings/LDM-2015-02-04.md | 294 ++++++++++++ meetings/LDM-2015-02-11.md | 305 ++++++++++++ meetings/LDM-2015-03-04.md | 140 ++++++ meetings/LDM-2015-03-10,17.md | 170 +++++++ meetings/LDM-2015-03-18.md | 186 ++++++++ meetings/LDM-2015-03-24.md | 100 ++++ meetings/LDM-2015-03-25 (Design Review).md | 173 +++++++ meetings/LDM-2015-03-25.md | 169 +++++++ meetings/LDM-2015-04-01,08.md | 138 ++++++ meetings/LDM-2015-04-14.md | 86 ++++ meetings/LDM-2015-04-15.md | 72 +++ meetings/LDM-2015-04-22 (Design Review).md | 61 +++ meetings/LDM-2015-05-20.md | 261 +++++++++++ meetings/LDM-2015-05-25.md | 54 +++ meetings/LDM-2015-07-01.md | 107 +++++ meetings/LDM-2015-07-07.md | 71 +++ meetings/LDM-2015-08-18.md | 65 +++ meetings/LDM-2015-09-01.md | 156 +++++++ meetings/LDM-2015-09-02.md | 102 ++++ meetings/LDM-2015-09-08.md | 173 +++++++ .../LDM-2015-10-07 (Design Review Notes).md | 135 ++++++ meetings/LDM-2015-11-02 (Design Demo).md | 251 ++++++++++ meetings/LDM-2016-02-29.md | 188 ++++++++ meetings/LDM-2016-04-06.md | 267 +++++++++++ meetings/LDM-2016-04-12-22.md | 225 +++++++++ meetings/LDM-2016-05-03,04.md | 109 +++++ meetings/LDM-2016-05-10.md | 52 +++ meetings/LDM-2016-07-12.md | 67 +++ meetings/LDM-2016-07-13.md | 115 +++++ meetings/LDM-2016-07-15.md | 120 +++++ meetings/LDM-2016-08-24.md | 69 +++ meetings/LDM-2016-09-06.md | 22 + meetings/LDM-2016-10-18.md | 68 +++ meetings/LDM-2016-10-25,26.md | 150 ++++++ meetings/LDM-2016-11-15.md | 89 ++++ meetings/LDM-2016-11-30.md | 126 +++++ meetings/LDM-2016-12-07,14.md | 162 +++++++ meetings/LDM-2017-01-17.md | 50 ++ 40 files changed, 5879 insertions(+) create mode 100644 meetings/LDM-2015-01-21.md create mode 100644 meetings/LDM-2015-01-28.md create mode 100644 meetings/LDM-2015-02-04.md create mode 100644 meetings/LDM-2015-02-11.md create mode 100644 meetings/LDM-2015-03-04.md create mode 100644 meetings/LDM-2015-03-10,17.md create mode 100644 meetings/LDM-2015-03-18.md create mode 100644 meetings/LDM-2015-03-24.md create mode 100644 meetings/LDM-2015-03-25 (Design Review).md create mode 100644 meetings/LDM-2015-03-25.md create mode 100644 meetings/LDM-2015-04-01,08.md create mode 100644 meetings/LDM-2015-04-14.md create mode 100644 meetings/LDM-2015-04-15.md create mode 100644 meetings/LDM-2015-04-22 (Design Review).md create mode 100644 meetings/LDM-2015-05-20.md create mode 100644 meetings/LDM-2015-05-25.md create mode 100644 meetings/LDM-2015-07-01.md create mode 100644 meetings/LDM-2015-07-07.md create mode 100644 meetings/LDM-2015-08-18.md create mode 100644 meetings/LDM-2015-09-01.md create mode 100644 meetings/LDM-2015-09-02.md create mode 100644 meetings/LDM-2015-09-08.md create mode 100644 meetings/LDM-2015-10-07 (Design Review Notes).md create mode 100644 meetings/LDM-2015-11-02 (Design Demo).md create mode 100644 meetings/LDM-2016-02-29.md create mode 100644 meetings/LDM-2016-04-06.md create mode 100644 meetings/LDM-2016-04-12-22.md create mode 100644 meetings/LDM-2016-05-03,04.md create mode 100644 meetings/LDM-2016-05-10.md create mode 100644 meetings/LDM-2016-07-12.md create mode 100644 meetings/LDM-2016-07-13.md create mode 100644 meetings/LDM-2016-07-15.md create mode 100644 meetings/LDM-2016-08-24.md create mode 100644 meetings/LDM-2016-09-06.md create mode 100644 meetings/LDM-2016-10-18.md create mode 100644 meetings/LDM-2016-10-25,26.md create mode 100644 meetings/LDM-2016-11-15.md create mode 100644 meetings/LDM-2016-11-30.md create mode 100644 meetings/LDM-2016-12-07,14.md create mode 100644 meetings/LDM-2017-01-17.md diff --git a/meetings/LDM-2015-01-21.md b/meetings/LDM-2015-01-21.md new file mode 100644 index 0000000000..1c42d57686 --- /dev/null +++ b/meetings/LDM-2015-01-21.md @@ -0,0 +1,434 @@ +C# Design Meeting Notes for Jan 21, 2015 +======================================== + +Discussion thread on these notes can be found at https://github.com/dotnet/roslyn/issues/98. + +Quotes of the day: + +> Live broadcast of design meetings: we could call it C#-SPAN +> +> We've made it three hours without slippery slopes coming up! + + +Agenda +------ + +This is the first design meeting for the version of C# coming after C# 6. We shall colloquially refer to it as C# 7. The meeting focused on setting the stage for the design process and homing in on major themes and features. + +1. Design process +2. Themes +3. Features + +See also [Language features currently under consideration by the language design group](https://github.com/dotnet/roslyn/issues?q=is%3Aopen+label%3A%22Area-Language+Design%22+label%3A%221+-+Planning%22+ "Language Features Under Consideration"). + +1. Design process +================= + +We have had great success sharing design notes publicly on CodePlex for the last year of C# 6 design. The ability of the community to see and respond to our thinking in real time has been much appreciated. + +This time we want to increase the openness further: + +- we involve the community from the beginning of the design cycle (as per these notes!) +- in addition to design notes (now issues on GitHub) we will maintain feature proposals (as checked-in Markdown documents) to reflect the current design of the feature +- we will consider publishing recordings of the design meetings themselves, or even live streaming +- we will consider adding non-Microsoft members to the design team. + +Design team +----------- + +The C# 7 design team currently consists of + +- [Anders Hejlsberg](https://github.com/ahejlsberg) +- [Mads Torgersen](https://github.com/MadsTorgersen) +- [Lucian Wischik](https://github.com/ljw1004) +- [Matt Warren](https://github.com/mattwar) +- [Neal Gafter](https://github.com/gafter) +- [Anthony D. Green](https://github.com/AnthonyDGreen) +- [Stephen Toub](https://github.com/stephentoub) +- [Kevin Pilch-Bisson](https://github.com/Pilchie) +- [Vance Morrison](https://github.com/vancem) +- [Immo Landwerth](https://github.com/terrajobst) + +Anders, as the chief language architect, has ultimate say, should that ever become necessary. Mads, as the language PM for C#, pulls together the agenda, runs the meetings and takes the notes. (Oooh, the power!) + +To begin with, we meet 4 hours a week as we decide on the overall focus areas. There will not be a separate Visual Basic design meeting during this initial period, as many of the overall decisions are likely to apply to both and need to happen in concert. + +Feature ideas +------------- + +Anyone can put a feature idea up as an *issue* on GitHub. We'll keep an eye on those, and use them as input to language design. + +A way to gauge interest in a feature is to put it up on UserVoice, where there's a voting system. This is important, because the set of people who hang out in our GitHub repo are not necessarily representative of our developer base at large. + +Design notes +------------ + +Design notes are point-in-time documents, so we will put them up as *issues* on GitHub. For a period of time, folks can comment on them and the reactions will feed into subsequent meetings. + +Owners and proposals +-------------------- + +If the design team decides to move on with a feature idea, we'll nominate an *owner* for it, typically among the design team members, who will drive the activities related to the design of that feature: gathering feedback, making progress between meetings, etc. Most importantly, the owner will be responsible for maintaining a *proposal* document that describes the current state of that feature, cross-linking with the design notes where it was discussed. + +Since the proposals will evolve over time, they should be documents in the repo, with history tracked. When the proposal is first put up, and if there are major revisions, we will probably put up an issue too, as a place to gather comments. There can also be pull requests to the proposals. + +We'll play with this process and find a balance. + +Other ways of increasing openness +--------------------------------- + +We are very interested in other ideas, such as publishing recordings (or even live streaming?) of the design meeting themselves, and inviting non-Microsoft luminaries, e.g., from major players in the industry, onto the design team itself. We are certainly open to have "guests" (physical or virtual) when someone has insights that we want to leverage. + +However, these are things we can get to over time. We are not going to do them right out of the gate. + +Decisions +--------- + +It's important to note that the C# design team is still in charge of the language. This is not a democratic process. We derive immense value from comments and UserVoice votes, but in the end the governance model for C# is benevolent dictatorship. We think design in a small close-knit group where membership is long-term is the right model for ensuring that C# remains tasteful, consistent, not too big and generally not "designed by committee". + +If we don't agree within the design team, that is typically a sign that there are offline activities that can lead to more insight. Usually, at the end of the day, we don't need to vote or have the Language Allfather make a final call. + +Prototypes +---------- + +Ideally we should prototype every feature we discuss, so as to get a good feel fro the feature and allow the best possible feedback from the community. That may note be realistic, but once we have a good candidate feature, we should try to fly it. + +The cost of the prototyping is an issue. This may be feature dependent: Sometimes you want a quick throwaway prototype, sometimes it's more the first version of an actual implementation. + +Could be done by a member of the design team, the product team or the community. + +Agenda +------ + +It's usually up to Mads to decide what's ready to discuss. Generally, if a design team member wants something on the agenda, they get it. There's no guarantee that we end up following the plan in the meeting; the published notes will just show the agenda as a summary of what was *actually* discussed. + + +2. Themes +========= + +If a feature is great, we'll want to add it whether it fits in a theme or not. However, it's useful to have a number of categories that we can rally around, and that can help select features that work well together. + +We discussed a number of likely themes to investigate for C# 7. + +Working with data +----------------- + +Today’s programs are connected and trade in rich, structured data: it’s what’s on the wire, it’s what apps and services produce, manipulate and consume. + +Traditional object-oriented modeling is good for many things, but in many ways it deals rather poorly with this setup: it bunches functionality strongly with the data (through encapsulation), and often relies heavily on mutation of that state. It is "behavior-centric" instead of "data-centric". + +Functional programming languages are often better set up for this: data is immutable (representing *information*, not *state*), and is manipulated from the outside, using a freely growable and context-dependent set of functions, rather than a fixed set of built-in virtual methods. Let’s continue being inspired by functional languages, and in particular other languages – F#, Scala, Swift – that aim to mix functional and object-oriented concepts as smoothly as possible. + +Here are some possible C# features that belong under this theme: + +- pattern matching +- tuples +- "denotable" anonymous types +- "records" - compact ways of describing shapes +- working with common data structures (List/Dictionary) +- extension members +- slicing +- immutability +- structural typing/shapes? + +A number of these features focus on the interplay between "kinds of types" and the ways they are used. It is worth thinking of this as a matrix, that lets you think about language support for e.g. denoting the types (*type expressions*), creating values of them (*literals*) and consuming them with matching (*patterns*) : + +| Type | Denote | Create | Match | +|------------|-------------------------|------------------------------|--------------------------| +| General | `T` | `new T()`, `new T { x = e }` | `T x`, `var x`, `*` | +| Primitive | `int`, `double`, `bool` | `5`, `.234`, `false` | `5`, `.234`, `false` | +| String | `string` | `"Hello"` | `"Hello"` | +| Tuple | `(T1, T2)` | `(e1, e2)` | `(P1, P2)` | +| Record | `{ T1 x1, T2 x2 }` | `new { x1 = e1, x2 = e2 }` | `{ x1 is P1, x2 is P2 }` | +| Array | `T[]` | `new T[e]`, `{ e1, e2 }` | `{ P1, P2 }`, `P1 :: P2` | +| List | ? | ? | ? | +| Dictionary | ? | ? | ? | +| ... | | | | + + +A lot of the matrix above is filled in with speculative syntax, just to give an idea of how it could be used. + +We expect to give many of the features on the list above a lot of attention over the coming months: they have a lot of potential for synergy if they are designed together. + +Performance and reliability (and interop) +----------------------------------------- + +C# and .NET has a heritage where it sometimes plays a bit fast and loose with both performance and reliability. + +While (unlike, say, Java) it has structs and reified generics, there are still places where it is hard to get good performance. A top issue, for instance is the frequent need to copy, rather than reference. When devices are small and cloud compute cycles come with a cost, performance certainly starts to matter more than it used to. + +On the reliability side, while (unlike, say, C and C++) C# is generally memory safe, there are certainly places where it is hard to control or trust exactly what is going on (e.g., destruction/finalization). + +Many of these issues tend to show up in particular on the boundary to unmanaged code - i.e. when doing interop. Having coarse-grained interop isn't always an option, so the less it costs and the less risky it is to cross the boundary, the better. + +Internally at Microsoft there have been research projects to investigate options here. Some of the outcomes are now ripe to feed into the design of C# itself, while others can affect the .NET Framework, result in useful Roslyn analyzers, etc. + +Over the coming months we will take several of these problems and ideas and see if we can find great ways of putting them in the hands of C# developers. + +Componentization +---------------- + +The once set-in-stone issue of how .NET programs are factored and combined is now under rapid evolution. + +With generalized extension members as an exception, most work here may not fall in the language scope, but is more tooling-oriented: + +- generating reference assemblies +- static linking instead of IL merge +- determinism +- NuGet support +- versioning and adaptive light-up + +This is a theme that shouldn't be driven primarily from the languages, but we should be open to support at the language level. + +Distribution +------------ + +There may be interesting things we can do specifically to help with the distributed nature of modern computing. + +- Async sequences: We introduced single-value asynchrony in C# 5, but do not yet have a satisfactory approach to asynchronous sequences or streams +- Serialization: we may no longer be into directly providing built-in serialization, but we need to make sure we make it reasonable to custom-serialize data - even when it's immutable, and without requiring costly reflection. + +Also, await in catch and finally probably didn't make it into VB 14. We should add those the next time around. + +Metaprogramming +--------------- + +Metaprogramming has been around as a theme on the radar for a long time, and arguably Roslyn is a big metaprogramming project aimed at writing programs about programs. However, at the language level we continue not to have a particularly good handle on metaprogramming. + +Extention methods and partial classes both feel like features that could grow into allowing *generated* parts of source code to merge smoothly with *hand-written* parts. But if generated parts are themselves the result of language syntax - e.g. attributes in source code, then things quickly get messy from a tooling perspective. A keystroke in file A may cause different code to be generated into file B by some custom program, which in turn may change the meaning of A. Not a feedback loop we're eager to have to handle in real time at 20 ms keystroke speed! + +Oftentimes the eagerness to generate source comes from it being too hard to express your concept beautifully as a library or an abstraction. Increasing the power of abstraction mechanisms in the language itself, or just the syntax for applying them, might remove a lot of the motivation for generated boilerplate code. + +Features that may reduce the need for boilerplate and codegen: + +- Virtual extension methods/default interface implementations +- Improvements to generic constraints, e.g.: + - generic constructor constraints + - delegate and enum constraints + - operators or object shapes as constraints (or interfaces), e.g. similar to C++ concepts +- mixins or traits +- delegation + +Null +---- + +With null-conditional operators such as `x?.y` C# 6 starts down a path of more null-tolerant operations. You could certainly imagine taking that further to allow e.g. awaiting or foreach'ing null, etc. + +On top of that, there's a long-standing request for non-nullable reference types, where the type system helps you ensure that a value can't be null, and therefore is safe to access. + +Importantly such a feature might go along well with proper safe *nullable* reference types, where you simply cannot access the members until you've checked for null. This would go great with pattern matching! + +Of course that'd be a lot of new expressiveness, and we'd have to reconcile a lot of things to keep it compatible. In his [blog](http://blog.coverity.com/2013/11/20/c-non-nullable-reference-types), Eric Lippert mentions a number of reasons why non-nullable reference types would be next to impossible to fully guarantee. To be fully supported, they would also have to be known to the runtime; they couldn't just be handled by the compiler. + +Of course we could try to settle for a less ambitious approach. Finding the right balance here is crucial. + +Themeless in Seattle +-------------------- + +*Type providers*: This is a whole different kind of language feature, currently known only from F#. We wouldn't be able to just grab F#'s model though; there'd be a whole lot of design work to get this one right! + +*Better better betterness*: In C# we made some simplifications and generalizations to overload resolution, affectionately known as "better betterness". We could think of more ways to improve overload resolution; e.g. tie breaking on staticness or whether constraints match, instead of giving compiler errors when other candidates would work. + +*Scripting*: The scripting dialect of C# includes features not currently allowed in C# "proper": statements and member declarations at the top level. We could consider adopting some of them. + +*params IEnumerable*. + +*Binary literals and digit separators*. + + + +3. Features +=========== + +The Matrix above represents a feature set that's strongly connected, and should probably be talked about together: we can add kinds of types (e.g. tuples, records), we can add syntax for representing those types or creating instances of them, and we can add ways to match them as part of a greater pattern matching scheme. + +Pattern matching +---------------- + +Core then is to have a pattern matching framework in the language: A way of asking if a piece of data has a particular shape, and if so, extracting pieces of it. + +``` c# +if (o is Point(var x, 5)) ... +``` + +There are probably at least two ways you want to use "patterns": + +1. As part of an expression, where the result is a bool signaling whether the pattern matched a given value, and where variables in the pattern are in scope throughout the statement in which the pattern occurs. +2. As a case in a switch statement, where the case is picked if the pattern matches, and the variables in the pattern are in scope throughout the statements of that case. + +A strong candidate syntax for the expression syntax is a generalization of the `is` expression: we consider the type in an `is` expression just a special case, and start allowing any pattern on the right hand side. Thus, the following would be valid `is` expressions: + +``` c# +if (o is Point(*, 5) p) Console.WriteLine(o.x); +if (o is Point p) Console.WriteLine(p.x); +if (p is (var x, 5) ... +``` + +Variable declarations in an expression would have the same scope questions as declaration expressions did. + +A strong candidate for the switch syntax is to simply generalize current switch statements so that + +- the switch expression can be any type +- the case labels can contain patterns, not just constants +- the cases are checked in order of appearance, since they can now overlap + +``` c# +switch (o) { +case string s: + Console.WriteLine(s); + break; +case int i: + Console.WriteLine($"Number {i}"); + break; +case Point(int x, int y): + Console.WriteLine("({x},{y})"); + break; +case null: + Console.WriteLine("); + break +} +``` + +Other syntaxes you can think of: + +*Expression-based switch*: An expression form where you can have multiple cases, each producing a result value of the same type. + +*Unconditional deconstruction*: It might be useful to separate the deconstruction functionality out from the checking, and be able to unconditionally extract parts from a value that you know the type of: + +``` c# +(var x, var y) = getPoint(); +``` + +There is a potential issue here where the value could be null, and there's no check for it. It's probably ok to have a null reference exception in this case. + +It would be a design goal to have symmetry between construction and deconstruction syntaxes. + +Patterns *at least* have type testing, value comparison and deconstruction aspects to them. + +There may be ways for a type to specify its deconstruction syntax. + +In addition it is worth considering something along the lines of "active patterns", where a type can specify logic to determine whether a pattern applies to it or not. + +Imagine positional deconstruction or active patterns could be expressed with certain methods: + +``` c# +class Point { + public Point(int x, int y) {...} + void Deconstruct(out int x, out int y) { ... } + static bool Match(Point p, out int x, out int y) ... + static bool Match(JObject json, out int x, out int y) ... +} +``` + +We could imagine separate syntax for specifying this. + +One pattern that does not put new requirements on the type is matching against properties/fields: + +``` c# +if (o is Point { X is var x, Y is 0 }) ... +``` + +Open question: are the variables from patterns mutable? + +This has a strong similarity to declaration expressions, and they could coexist, with shared scope rules. + +Records +------- + +Let's not go deep on records now, but we are aware that we need to reconcile them with primary constructors, as well as with pattern matching. + +Array Slices +------------ + +One feature that could lead to a lot of efficiency would be the ability to have "windows" into arrays - or even onto unmanaged swaths of memory passed along through interop. The amount of copying that could be avoided in some scenarios is probably very significant. + +Array slices represent an interesting design dilemma between performance and usability. There is nothing about an array slice that is functionally different from an array: You can get its length and access its elements. For all intents and purposes they are indistinguishable. So the best user experience would certainly be that slices just *are* arrays - that they share the same type. That way, all the existing code that operates on arrays can work on slices too, without modification. + +Of course this would require quite a change to the runtime. The performance consequences of that could be negative even on the existing kind of arrays. As importantly, slices themselves would be more efficiently represented by a struct type, and for high-perf scenarios, having to allocate a heap object for them might be prohibitive. + +One intermediate approach might be to have slices be a struct type Slice, but to let it implicitly convert to T[] in such a way that the underlying storage is still shared. That way you can use Slice for high performance slice manipulation (e.g. in recursive algorithms where you keep subdividing), but still make use of existing array-based APIs at the cost of a boxing-like conversion allocating a small object. + +ref locals and ref returns +-------------------------- + +Just like the language today has ref parameters, we could allow locals and even return values to be by `ref`. This would be particularly useful for interop scenarios, but could in general help avoid copying. Essentially you could return a "safe pointer" e.g. to a slot in an array. + +The runtime already fully allows this, so it would just be a matter of surfacing it in the language syntax. It may come with a significant conceptual burden, however. If a method call can return a *variable* as opposed to a *value*, does that mean you can now assign to it?: + +``` c# +m(x, y) = 5; +``` + +You can now imagine getter-only properties or indexers returning refs that can be assigned to. Would this be quite confusing? + +There would probably need to be some pretty restrictive guidelines about how and why this is used. + +readonly parameters and locals +------------------------------ + +Parameters and locals can be captured by lambdas and thereby accessed concurrently, but there's no way to protect them from shared-mutual-state issues: they can't be readonly. + +In general, most parameters and many locals are never intended to be assigned to after they get their initial value. Allowing `readonly` on them would express that intent clearly. + +One problem is that this feature might be an "attractive nuisance". Whereas the "right thing" to do would nearly always be to make parameters and locals readonly, it would clutter the code significantly to do so. + +An idea to partly alleviate this is to allow the combination `readonly var` on a local variable to be contracted to `val` or something short like that. More generally we could try to simply think of a shorter keyword than the established `readonly` to express the readonly-ness. + +Lambda capture lists +-------------------- + +Lambda expressions can refer to enclosing variables: + +``` c# +var name = GetName(); +var query = customers.Where(c => c.Name == name); +``` + +This has a number of consequences, all transparent to the developer: +- the local variable is lifted to a field in a heap-allocated object +- concurrent runs of the lambda may access and even modify the field at the same time +- because of implementation tradeoffs the content of the variable may be kept live by the GC, sometimes even after lambdas directly using them cease to exist. + +For these reasons, the recently introduced lambdas in C++ offer the possibility for a lambda to explicitly specify what can be captured (and how). We could consider a similar feature, e.g.: + +``` c# +var name = GetName(); +var query = customers.Where([name]c => c.Name == name); +``` +This ensures that the lambda only captures `name` and no other variable. In a way the most useful annotation would be the empty `[]`, making sure that the lambda is never accidentally modified to capture *anything*. + +One problem is that it frankly looks horrible. There are probably other syntaxes we could consider. Indeed we need to think about the possibility that we would ever add nested functions or class declarations: whatever capture specification syntax we come up with would have to also work for them. + +C# always captures "by reference": the lambda can observe and effect changes to the original variable. An option with capture lists would be to allow other modes of capture, notable "by value", where the variable is copied rather than lifted: +``` c# +var name = GetName(); +var query = customers.Where([val name]c => c.Name == name); +``` +This might not be *too* useful, as it has the same effect as introducing another local initialized to the value of the original one, and then capture *that* instead. + +If we don't want capture list as a full-blown feature, we could consider allowing attributes on lambdas and then having a Roslyn analyzer check that the capture is as specified. + +Method contracts +---------------- + +.NET already has a contract system, that allows annotation of methods with pre- and post-conditions. It grew out of the Spec# research project, and requires post-compile IL rewriting to take full effect. Because it has no language syntax, specifying the contracts can get pretty ugly. + +It has often been proposed that we should add specific contract syntax: +``` c# +public void Remove(string item) + requires item != null + ensures Count >= 0 +{ + ... +} + +``` + +One radical idea is for these contracts to be purely runtime enforced: they would simply turn into checks throwing exceptions (or FailFast'ing - an approach that would need further discussion, but seems very attractive). + +When you think about how much code is currently occupied with arguments and result checking, this certainly seems like an attractive way to reduce code bloat and improve readability. + +Furthermore, the contracts can produce metadata that can be picked up and displayed by tools. + +You could imagine dedicated syntax for common cases - notably null checks. Maybe that is the way we get some non-nullability into the system? + diff --git a/meetings/LDM-2015-01-28.md b/meetings/LDM-2015-01-28.md new file mode 100644 index 0000000000..89f3a0bff5 --- /dev/null +++ b/meetings/LDM-2015-01-28.md @@ -0,0 +1,297 @@ +C# Design Meeting Notes for Jan 28, 2015 +======================================== + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/180. + +Quote of the day: + +> It's not turtles all the way down, it's frogs. :-) + +Agenda +------ + + +1. Immutable types +2. Safe fixed-size buffers +3. Pattern matching +4. Records + +See also [Language features currently under consideration by the language design group](https://github.com/dotnet/roslyn/issues?q=is%3Aopen+label%3A%22Area-Language+Design%22+label%3A%221+-+Planning%22+ "Language Features Under Consideration"). + +1. Immutable types +================== + +In research prototypes we've experimented with an `immutable` modifier on types, indicating that objects of the type are *deeply* immutable - they recursively do not point to mutable fields. Issue #159 describes the proposal in more detail. + +How do we construct types which once fully constructed can't be changed? + +- all fields are readonly, and recursively have immutable types +- can only inherit from other immutable types (or `object`) +- the constructor can't use "this" other than to access fields + +`unsafe` or some other notation could be used to escape scrutiny, in order to create "observable immutability" while cheating under the hood (typically for performance reasons). You could factor such unsafeness into a few types, e.g. `Lazy`. + +The feature is designed to work with generics. There would be a new constraint `where T: immutable`. However don't want to bifurcate on `Tuple` vs `ImmutableTuple` just based on whether the content type is constrained to `immutable`. + +Instead an immutable `Tuple` would instantiate to immutable types *only* if type arguments are all immutable. So `Tuple` would be immutable, `Tuple` wouldn't be. Immutable generic types would allow type parameters in fields, because that still maintains the recursive guarantee. + +Immutable interfaces are also part of the proposal. Somewhat strangely an immutable interface can only be implemented by types that pass all their non-immutable type parameters to the interface! + +What's the value? It's mostly a tool for the compiler to help you ensure you are following your intent of writing deeply immutable types. + +Why is that a valuable property to ensure in objects? +- it would allow safe parallel operations over them (modulo holes, see below) +- it would allow burning an object graph into the DLL by compile time evaluation of static initializers +- they can be passed to others without defensive copying + +Immutable delegates are ones that can only bind to methods on immutable types. At the language level, that means closures would need to be generated as immutable when possible - which it won't often be, unless we adopt readonly parameters and locals (#98, #115). + +As for choice of keyword: `readonly` indicates "shallow", that's why `immutable` may be a better word. + +Given the restrictions, you'd expect that any method call on an immutable type would have side effects only on data that was passed in to the method - so a parameterless method (or one taking only immutable parameters) would essentially be pure. + +Unfortunately that is not quite true. This expectation can be undermined by two things (other than the built-in facility for cheating): mutable static fields and reflection. We can probably live with reflection, that's already a way to undermine so many other language level expectations! However, mutable statics are unfortunate, not just for this scenario but in general. It would be a breaking change to start disallowing them, of course, bu they could be prevented with a Roslyn analyzer. + +Even then, while not having side effects, calling such a method twice with the same arguments might not yield the same result: even returning a new object isn't idempotent. + +Given the holes and gotchas, the question is whether it is still valuable enough to have this feature? If it's not a full guarantee but mostly a help to not make mistakes, maybe we should do this through attributes and analyzers? The problem with analyzers is that you can't rely on other folks to run them on their code that you depend on. It wouldn't e.g. prevent defensive copies. + +In our research project, this turned out to be very valuable in detecting bugs and missed optimization opportunities. + +The object freezing could be done without the feature just by carefully analyzing static fields. But the feature might better help people structure things to be ripe for it. + +IDE tooling benefits: Extract method would not need to grab all structs by ref. + +We would have to consider making it impossible to implement an immutable interface even via the old compiler. Otherwise there's a hole in the system. Something with "modrec"? + +If we added a bunch of features that introduce readonly objects to C# 7 (like records, tuples, ...) and then add this feature later, would we end up being in trouble? Only if we violated the rules we would end up applying. + +Marking an existing unsealed type as immutable would be a breaking change. If we introduce such classes in C# 7, it would be breaking to make them immutable later. + +As a case study, has Roslyn suffered from the lack of this feature? There have been race conditions, but those would still have happened. Is Roslyn not a great example? + +Probably not. Roslyn is *not* immutable. It's presenting an immutable *view*, but is mutable inside. Would that be the common case, though? + +Some of the "cheating" in Roslyn (caching, free lists, etc) is for performance, some is for representing cycles. Insofar as the immutable types feature is *also* for performance, it seems that thhere's a tension between using it or not. + +In summary, we are unsure of the value. Let's talk more. + + +2. Safe Fixed-Size buffers +========================== + +Why are fixed-size buffers unsafe? We could generate safe code for them - at least when not in unsafe regions. Proposal described at #126. + +It might generate a lot of code. You could do it with different syntax, or switch on how you generate. + +This is a very constrained scenario. It wouldn't be harmful, and a few people would celebrate, but is it worth our effort? + +It would allow arbitrary types, not just primitive like today. That may be the bigger value. + +Not a high-pri, not one of the first we should commit to. + + +3. Pattern matching +=================== + +A view on pattern matching: + +A pattern is not an expression, but a separate construct. It can be recursive. +It's idempotent, doesn't affect the state of the program or "compute" something. + +Sktech of possible pattern syntax: + +> *pattern:* +  `*` +  *literal* +  `var` *identifier* +  *type* *identifier*opt +  *type* `{` *member* `is` *pattern* ... `}` *identifier*opt + +> *expression:* +  *expression* `is` *pattern* + +> *switch-label:* +  `case` *pattern* `:` + + +Actually we'd separate into simple and complex patterns and only allow some at the top level. + +We'd have to think carefully about semantics to make it fit into existing is expressions and switch statements. Alternatively we'd come up with a new kind of switch statement. The syntax of switch is already among the least appealing parts of C# - maybe time for a revamp anyway? + +Additionally we could imagine a switch *expression*, e.g. of the form: + +``` c# +match (e) { pattern => expression; ... ; default => expression } +``` +This would result in a value, so it would have to be complete - exactly one branch would have to be taken. Maybe an expression would be allowed to throw. In fact, `throw` could be made an expression. + +Expanded `is` operator +---------------------- + +Here's an example scenario from the Roslyn framework. This should not be taken to say that this is a feature specific to building compilers, though! First, without pattern matching: + +``` c# +var e = s as ExpressionStatement; +if (e != null) { + var a = e.Expr as AssignmentExpressionSyntax; + if (a != null) { + var l = a.Left as IdentifierName; + var r = a.RIght as IdentifierName; + if (l != null && r != null & l.Name.name == r.Name.name) ... +``` + +Ugh! With just the `e is T x` non-recursive pattern match we can do a lot better, handling everything in a single if condition: + +``` c# +if (s is ExpressionStatement e && + e.Expr is AssignmentExpressionSyntax a && + a.Left is IdentifierName l && + a.Right is IdentifierName r && + l.Name.name == r.Name.name) ... +``` + +Much more readable! The explicit null checks and the nesting are gone, and everything still has sensible names. + +The test digs pretty deep inside the structure. Here's what if would look like with recursive `T { ... }` patterns: + +``` c# +if (s is ExpressionStatement { + Expr is AssignmentExpressionSyntax { + Left is IdentifierName { Name is val l }, + Right is IdentifierName { Name is val r } } } + && l.name = r.name) ... +``` + +Here the pattern match sort of matches the structure of the object itself, nesting patterns for nested objects. It is not immediately obvious which is more readable, though - at least we disagree enthusiastically on the design team about that. + +It is possible that the nesting approach has more things going for it, though: + +- patterns might not just be type tests - we could embrace "active patterns" that are user defined +- we could do optimized code gen here, since evaluation order might not necessarily be left to right. +- the code becomes more readable because the structure of the code matches the shape of the object. +- the approach of coming up with more top-level variable names may run out of steam the deeper you go. + +The `T { ... }` patterns are a little clunky, but would apply to any object. For more conciseness, we imagine that types could specify positional matching, which would be more compactly matched through a `T (...)` pattern.That would significantly shrink the recursive example: + +``` c# +if (s is ExpressionStatement( + AssignmentExpressionSyntax(IdentifierName l, IdentifierName r)) + && l.name = r.name) ... +``` + +This is more concise, but it relies on you knowing what the positions stand for since you are not giving them a name at the consumption site. + +So, in summary, it's interesting that the `e is T x` form in itself gives most of the value. The incremental improvement of having recursive patterns may not be all that significant. + +Either way, the built-in null check is likely to help with writing null-safe code. + +On the tooling side, what would the stepping behavior be? One answer is that there is no stepping behavior. That's already the case inside most expressions. Or we could think it through and come up with something better if the need is there. + +The variables are definitely assigned only when true. There is a scoping issue with else if. It's similar to the discussions around declaration expressions in C# 6: + +``` c# +if (o is string s) { ... s ... } +else if (o is short s) { ... s ... } // error: redeclaration of s +else ... +``` + +All else equal, though not definitely assigned there, the `string s` introduced in the first if condition would also be in scope in the else branch, and the nested if clause would therefore not be allowed to introduce another variable with the same name `s`. + +With declaration expressions we discussed having special rules around this, e.g. making variables introduced in an if condition *not* be in scope in the else clause. But this gets weird if you try to negate the condition and swap the branches: all of a sudden the variables would be in scope only where they are *not* definitely assigned. + +Expanded `switch` statement +--------------------------- + +The above examples are in the context of if(...is...), but for switch statements you can't just put `&&...` after the initial pattern. Maybe we would allow a `where ...` (or `when`) in the case labels to add additional filtering: + +``` c# +switch (o) { +case ExpressionStatement( + AssignmentExpressionSyntax(IdentifierName l, IdentifierName r) + where (l.name == r.name): + ... +} +``` + +If we add pattern matching to switch statements, one side effect is that we generalize the type of thing you can switch on to anything classified as a value. + +``` c# +object o = ... +switch(o) { + case 1: + case 2: + case 3: + case Color.Red: + case string s: + case *: // no + case var x: // would have to be last and there'd have to not be a default: + default: +} +``` +We would probably disallow `*`, the wildcard, at the top level. It's only useful in recursive positional notation. + +Evaluation order would now be important. For back compat, we would allow default everywhere, and evaluate it last. + +We would diagnose situations where an earlier pattern hides a later one: + +``` c# +case Point(*, 2): +case Point(1, 2): // error/warning +``` + +We could allow case guards, which would make the case not subsume other cases: + +``` c# +case Point(var x, 2) where (x > 2): +case Point(1, 2): // fine +``` + +User-defined `is` operator +-------------------------- + +We've sneaked uses of a positional pattern match above. For that to work, a type would have to somehow specify which positional order to match it in. One very general approach to this would be to allow declaration of an `is` operator on types: + +``` c# +public class Point +{ + public Point(int x, int y) { this.X = x; this.Y = y; } + public int X { get; } + public int Y { get; } + overrides public int GetHashCode() ... + overrides public bool Equals(...)... + public static bool operator is(Point self out int x, out int y) {...} +} +``` + +The operator `is` is a particular way of specifying custom matching logic, similar to F# active patterns. We could imagine less ambitious ways, in particular if we just want to specify deconstruction and not additional logic. That's something to dive into later. + +4. Records +========== + +A value-semantics class like the above would be automatically generated by a "record" feature, e.g. from something like: + +``` c# +class Point(int X, int Y); +``` + +By default, this would generate all of the above, except parameter names would be upper case. If you want to supercede default behavior, you can give it a body and do that explicitly. For instance, you could make X mutable: + +``` c# +class Point(int X, int Y) +{ + public int X { get; set; } = X; +} +``` + +You could have syntax to create separate parameter names from member names: + +``` c# +class Point(int x:X, int y:Y) +{ + public int X { get; set; } = x; +} +``` + +Whether in this form or otherwise, we definitely want to pursue more concise type declarations that facilitate value semantics and deconstruction. We want to pursue the connection with anonymous types and we want to pursue tuples that mesh well with the story too. + diff --git a/meetings/LDM-2015-02-04.md b/meetings/LDM-2015-02-04.md new file mode 100644 index 0000000000..44768eb233 --- /dev/null +++ b/meetings/LDM-2015-02-04.md @@ -0,0 +1,294 @@ +C# Design Meeting Notes for Feb 4, 2015 +======================================== + +Discussion thread on these notes can be found at https://github.com/dotnet/roslyn/issues/396. + +Agenda +------ + +1. Internal Implementation Only (C# 6 investigation) +2. Tuples, records and deconstruction +3. Classes with value semantics + + +1. Internal implementation only +=============================== + +We have a versioning problem in the Roslyn APIs that we suspect is somewhat common - that of inherited hierarchies. + +The essence of the problem is that we want to describe an inheritance *hierarchy* of types in the abstract, as well as several concrete "implementations" of the hierarchy. + +In Roslyn, the main example is that we have a language agnostic hierarchy of symbols, as well as a C# and VB specific implementation of that. + +This leads to a need for multiple inheritance: "C#-specific local symbol" needs to both inherit "C#-specific symbol" and "abstract local symbol". The only way to represent this is to express the abstract hierarchy with interfaces: + +``` c# +public interface ISymbol { ... } +public interface ILocalSymbol : ISymbol { ... } + +public abstract class CSharpSymbol : ISymbol { ... } +public class CSharpLocalSymbol : CSharpSymbol, ILocalSymbol { ... } + +// Etc... +``` + +So far, so good. However, even though `ISymbol` and friends are interfaces, we don't actually want anyone else to implement them. They are not intended as extension points, we merely made them interfaces to allow multiple base types. On the contrary we would like to be able to add members to these types in the future, as the Roslyn API evolves. + +Had these types been abstract classes, we could have prevented implementation from other assemblies by having only internal constructors. That would have protected the types for future extensions. However, for interfaces no such trick exists. + +As a result, if we ever want to evolve these interfaces by adding more members, but someone implemented them *despite our intent*, we will break those people. + +Our options, current and future, seem to include: + +1. Tell people really loudly in comments on the interfaces that they shouldn't implement them, because we will break them +2. Add an attribute, `ImplicitImplementationOnlyAttribute`, to the interfaces +3. Write an analyzer that enforces the attribute, make it be installed and on by default and hope that catches enough cases +4. Make the compiler enforce the attribute +5. Add a mechanism to the language to safely evolve interfaces + * default implementations of interface methods (like in Java) - will require CLR support +6. Add an alternative multiple-inheritance mechanism to the language + * mixins or traits + +Options 1, 2 and 3 are open to us today. However, the concern is whether we can consider them strong enough discouragement that our future selves will feel good about evolving the interfaces. If we do 1, 2 and 3, will we then be ready to later break people who disregard the discouragement? + +We have never been 100% on compat. Reliance on reflection, codegen patterns etc, can cause code to be breakable today. The BCL has a clear set of guidelines of which things they allow themselves to change even if they can break existing code: adding private fields, etc. + +Their rule on interfaces is that we cannot add members to an interface. In inherited-hierarchies situations such as the one in Roslyn that constraint is hard to live with. + +Analyzers +--------- + +Analyzers can easily detect when an interface with the attribute is being implemented outside of its assembly. + +The problem here is that it is easy to land in a situation where you do not have the analyzer, or it is not turned on. Analyzers are optional by design: they help you use an API, but the API author can't rely on them being enforced. + +Compiler enforcement +-------------------- + +Moving this check into the compiler would make the "hole" smaller, but, perhaps surprisingly, wouldn't completely deal with it: a pre C# 6 compiler would still happily compile an implementation of an interface with the attribute on. Now, upgrading to C# 6 and the Roslyn-based compiler would be a breaking change! + +Also, this may play badly with mocking. Though many of the automated mocking frameworks already have a way to work around internal requirements, explicit or manual mocking would still suffer. + +There are probably some design details around how this works in conjunction with `InternalsVisibleTo`: + +Assembly A: + +``` c# +[assembly: InternalVisibleTo("B")] + +[InternalImplementationOnly] +public interface IA {} +``` + +Assembly B: + +``` c# +public abstract class B : IA {} +public interface IB : IA {} // implicitly inherits restriction? +``` + +Assembly C: + +``` c# +public class C : B {} // OK +public class D : B, IA {} // not OK +public class E : IB {} // not OK +``` + +Conclusion +---------- + +We need to talk to the BCL team to decide whether we would consider either of these approaches sufficient to protect the evolvability of interfaces. + + + +2. Tuples, records, deconstruction +================================== + +We are eager to look at language support for tuples. They should facilitate multiple return values from methods, including async ones, and a consumption experience that includes deconstruction. + +We'll have a proposal ready to discuss in the next meeting (It is now here: #347). + +We are also interested in making it much easier to write something akin to algebraic datatypes (discriminated unions), whose shape and contents are easily pattern-matched on and, probably, deconstructed. + +While we have a proposal for records that caters to that (#206), there's a sense that this might be more closely connected with the tuple feature than the current proposals suggest, and we should churn on these together to produce a unified, or at least rationalized, design. + +Are tuples just a specific kind of record? Are records just tuples with a name? Further exploration is needed! + +The next point is one such exploration. + + +3. Classes with values +====================== + +We explored a possible pattern for immutable classes. It may or may not be the right thing to do, but it generated a lot of ideas for features that would be possible with it, and that it would be interesting to pursue regardless of the pattern. + +The core idea is: if you want value semantics for your object, maybe your object should literally have a `Value`: + +``` c# +public class Person +{ + public readonly PersonValue Value; + public Person(PersonValue value) { Value = value; } + public string Name => Value.Name; + public int Age => Value.Age; + … + public struct PersonValue + { + public string Name; + public int Age; + } +} +``` + +The core idea is that there is a *mutable* value type representing the actual state. The object wraps a `readonly` field `Value` of that value type and exposes properties that just delegate to the `Value`. + +This starts out with the obvious downside that there are two types instead of one. We'll get back to that later. + + +Value semantics +--------------- + +Value semantics mean a) being immutable and b) having value-based equality (and hash code). Value types *already* have value-based equality and hash code, and the `Value` field is `readonly`. So instead of having to negotiate computation of hashcodes, a developer using this pattern can just forward these questions to the `Value`: + +``` c# +public override bool Equals(object other) => (other as Person)?.Value.Equals(Value) ?? false; // or similar +public override int GetHashCode() => Value.GetHashCode; +``` + +No need to write per-member code. All is taken care of by the `Value`. So for bigger types this definitely leads to less code bloat. + +You could implement `IEquatable` for better performance, but this illustrates the point. + + +Builder pattern +--------------- + +This approach is essentially a version of the Builder pattern: `PersonValue` acts as a builder for `Person` in that it can be manipulated, and eventually passed to the constructor (which implements the Parameter Object pattern by taking all its parameters as one object). + +This allows for the use of object initializers in creating new instances: + +``` c# +var person = new Person(new PersonValue { Name = "John Doe" }); +``` + +It also lets you create new objects from old once using mutation to incrementally change it: + +``` c# +var builder = Person.Value; +builder.Name = "John Deere"; +person = new Person(builder); +``` + +This again scales to much larger objects, and illustrates one of the points why the builder pattern is popular. Another popular approach is to use "Withers", methods for returning a new instance of an immutable type that's incrementally changed in one way from an old one: + +``` c# +person.WithName("John Deere"); +``` + +Withers are more elegant to use, at least when you are only changing one property. But when changing multiple ones, you need to chain them, creating intermediate objects along the way. What's worse, you need to declare a `WithFoo` method for every single property, which is a lot of boilerplate. + +Maybe we can make the Builder pattern more elegant to use - more like the Wither pattern? + + +Object initializers +------------------- + +One idea is to allow two new uses of object initializers: + +* when creating new immutable objects that implement the builder pattern +* when non-destructively "modifying" immutable objects by creating new ones with deltas + +First, let's allow this (similar to what's proposed in #229): + +``` c# +var person = new Person { Name = "John Doe" }; +``` + +It would simply rewrite to this: + +``` c# +var person = new Person(new PersonValue { Name = "John Doe" }); +``` + +Which is what we had above. + +Second, let's allow object initializers on *existing* values instead of just new expressions. This is a much requested feature already, because it would apply to factories etc. + +It would allow us to do a lot better on the incremental modification: + +``` c# +person = new Person(person.Value { Name = "John Deere" }); +``` + +If we are uncomfortable just letting object initializers occur directly on any expression, we could consider adding a keyword, like `with`. + +Now we can get the builder, modify it and create a new Person from it, all in one expression. + +But of course the kicker is to combine the two ideas, allowing you to write: + +``` c# +person = person { Name = "John Deere" }; +``` + +to the same effect: + +* Since `person` doesn't have a writable member `Name`, get its builder +* modify the builder based on the object initializer +* create a new `Person` from the builder + +So in summary, adding the builder patter in *some* compiler-recognized form allows us to tinker with the object initializer feature, making it work as well for immutable objects as it does for mutable ones, and essentially serving as built-in "wither" support. + + +Using tuples as the values +-------------------------- + +Tuples are far from a done deal, but assuming a design like #347, where tuples are mutable structs, they could actually serve as the type of the `Value` field, thus obliterating the need for a second type declaration: + +``` c# +public class Person +{ + public readonly (string Name, int Age) Value; // a tuple + public Person((string Name, int Age) value) { Value = value; } + … +} +``` + +This opens up one more opportunity. Assuming you want your immutable type to be deconstructable the same way as a tuple - by position. How do you indicate that? Well maybe having a tuple as your `Value` is how: deconstruction is simply yet another aspect that is delegated to the `Value`: + +``` c# +(var n, var a) = GetPerson(); // deconstructed as per the `Value` tuple +``` + +Records +------- + +If we have a pattern that the compiler supports in various ways for immutable types (whether this pattern or another), it makes sense that that pattern should also underlie any record syntax that we add. + +Assuming a record syntax like in #206, the record definition: + +``` c# +class Person(string Name, int Age); +``` + +Would then generate the class + +``` c# +class Person +{ + public readonly (string Name, int Age) Value; + public Person((string Name, int Age) value) { Value = value; } + public string Name => Value.Name; + public int Age => Value.Age; + public override bool Equals(object other) => (other as Person)?.Value.Equals(Value) ?? false; // or similar + public override int GetHashCode() => Value.GetHashCode; +} +``` + +And would therefore support object initializers and positional deconstruction. + + +Conclusion +---------- + +We are not at all sure if this is the right general direction, let alone the right *specific* pattern if it is. But builders are a common pattern, and for a reason. It would be interesting if support for them was somehow included in what we do for immutable types in C# 7. diff --git a/meetings/LDM-2015-02-11.md b/meetings/LDM-2015-02-11.md new file mode 100644 index 0000000000..e82a75bc22 --- /dev/null +++ b/meetings/LDM-2015-02-11.md @@ -0,0 +1,305 @@ +C# Design Meeting Notes for Feb 11, 2015 +========================================= + +Discussion on these notes can be found at https://github.com/dotnet/roslyn/issues/1207. + +Agenda +------ + +1. Destructible types <*we recognize the problem but do not think this is quite the right solution for C#*> +2. Tuples <*we like the proposal, but there are several things to iron out*> + + + +1. Destructible types +===================== + +Issue #161 is a detailed proposal to add destructible types to C#. + +The problem area is deterministic disposal of resources. + +Finalizers are notoriously non-deterministic (you can never know when or even whether they run), and rampant usage is quite a hit on performance. `using` statements are the language's current attempt at providing for deterministic disposal. + +The problem with `using` statements is that no-one is forced to use them. An `IDisposable` resource is disposed of only if people remember to wrap it in a `using`. FxCop rules and now Roslyn diagnostics can be employed to help spot places where things aren't disposed that should be, but these have notoriously run into false positives and negatives, to the extent that they are a real nuisance and often get turned off. + +At least one reason for this is the lack of their ability to track ownership. Whose responsibility is it to ultimately dispose the resource, and how is that responsibility transferred? + +Destructible types offer a sweeping approach to deal with this, that is a complete departure from `using` and `IDisposable` and instead leans more closely on C++-like RAII features. + +The core idea is that destructible types are a language concept (they have to be declared as such): + +``` c# +destructible struct Handle +{ + IntPtr ptr; + + ~Handle() + { + if (ptr != IntPtr.Zero) Free(ptr); + } +} +``` + +Destructible types can only be fields in other destructible types. + +Variables of destructible type have their contents automatically disposed when they go out of scope. + +To prevent multiple disposal, such variables can't just be assigned to others. Instead, ownership must be explicitly transferred (with a `move` keyword): + +``` c# +void M() +{ + Handle h = CreateHandle(); + Handle h2 = h; // Not allowed! Moves are explicit + Handle h3 = move h; // Explicit owner transfer + + M2(new Handle()); // ok, no owner + M2(h3); // Not allowed! + M2(move h3); // ok, explicit owner transfer + +} +void M2(Handle h) ... +``` + +At the end of M, all locals with destructible types are destructed, in opposite order of their introduction. + +Additionally variables can be "borrowed" by being passed by `ref` to a method. + +There are a lot of limitations to the feature, and it doesn't replace `IDisposable`, because you cannot use destructible types as type arguments, in arrays, and as fields of non-destructible types. Also, they cannot implement interfaces. + +As a release valve one can have a `Box` that "magically" transfers the ownership of a destructible type to the finalizer. The finalizer will crash the process if the destructible hasn't been properly destructed. + +There has been some negative feedback around the destruction being implicit, even as the ownership transfer was explicit. When assignment overwrites a value, that value has to be destructed. But what about cases like this? + +``` c# +{ + D x; + if (e) x = new x(); + x = new x(); +} +``` + +How to know whether the second assignment overwrites a value, that should therefore be destructed? We'd have to keep track at runtime. Or prevent this situation through definitely assigned or unassigned rules, or the like. + + +Conclusion +---------- + +Overall, we see how this adds value, but not enough that having these concepts in the face of all C# developers. It doesn't earn back it's -100 points. + +Could we take a step back and have analyzers encourage practices that are less error prone? Yes. You just cannot have analyzers enforce correctness. And you cannot have them improve perf by affecting codegen. And they cannot do the heavy lifting for you. + +An analyzer can help you with where to put your `using` statements. FxCop tries to do that today, but has too many false positives (use vs ownership). Even if you could get rid of those, there's a lot of code you'd need to write. + +People do struggle with `IDisposable` today. They don't know when things are `IDisposable`, whether they are the ones who should dispose things. + +We would like to solve the problem in C#, and pick over this proposal for ideas, but we'd need to noodle with it to make it a better fit. It would need to integrate better with current `IDisposable`. + + + +2. Tuples +========= + +Issue #347 is a proposal for adding tuples to C#. The main guiding principle for the proposal is to enable multiple return values, and wherever possible design them in analogy with parameter lists. + +As such, a tuple type is a parenthesized list of types and names, just like a parameter list: + +``` c# +public (int sum, int count) Tally(IEnumerable values) { ... } + +var t = Tally(myValues); +Console.WriteLine($"Sum: {t.sum}, count: {t.count}"); +``` + +Along the same lines, tuple values can be constructed using a syntax similar to argument lists. By position: + +``` c# +public (int sum, int count) Tally(IEnumerable values) +{ + var sum = 0; var count = 0; + foreach (var value in values) { sum += value; count++; } + return (sum, count); // target typed to the return type +} +``` + +Or by name: + +``` c# +public (int sum, int count) Tally(IEnumerable values) +{ + var res = (sum: 0, count: 0); // infer tuple type from names and values + foreach (var value in values) { res.sum += value; res.count++; } + return res; +} +``` + +Finally, there'd be syntax to deconstruct tuples into variables for the individual members: + +``` c# +(var sum, var count) = Tally(myValues); // deconstruct result +Console.WriteLine($"Sum: {sum}, count: {count}"); +``` + +Tuples should be thought of as temporary, ephemeral constellations of values without inherent conceptual connection: they just "happen to be traveling together". + +We are generally eager to pursue this design. There are a number of open issues, that we explore in the following. + + +Type equivalence +---------------- + +Intuitively you'd expect that every time you use the same tuple type expression, it denotes the same type - that there is *structural equivalence*. However, depending on how we implement tuple types on top of the CLR, that may not always be easy to achieve - especially when unifying across different assemblies. + +To take it to an extreme, what if they didn't even unify *within* an assembly? Even then they'd probably have significant value. They would be good for the "ephemeral" core scenario of things traveling together. It wouldn't let them serve as e.g. builders; they would really only work for the multiple return values scenario. + +Even then there'd be issues with virtual method overrides, etc: When you override and restate the return type, it doesn't really work if that declares a *different* return type. + +``` c# +class C +{ + public abstract (int x, int y) GetCoordinates(); +} +class D : C +{ + public override (int x, int y) GetCoordinates() { ... } // Error! different return type!!?! +} +``` + +A middle ground is for these methods to unify *within* but not *across* assemblies. This is what we do for anonymous types today. However, there we are careful never to let the types occur in member signatures, whereas that would be the whole point of tuples. + +This makes a little more sense than no unification at all, since at least all mentions of a tuple type within the same member body and even type body refer to the same type. However there'd still be weird behavior involving overrides across assembly boundaries, equivalence of generic types constructed with tuple types, etc. We could try to be smart about reusing types from other assemblies when possible, etc, but it would be quite a game of whack-a-mole to get all the edge cases behaving sensibly. + +The only thing that really makes sense at the language level is true structural equivalence. Unfortunately, the CLR doesn't really provide for unification of types with similar member names across assemblies. Ideally we could *add* that to the CLR, but adding new functionality to the CLR is not something to be done lightly, as it breaks all downlevel targeting. + +A way to get structural equivalence working at compile time would be to encode tuples with framework types similar to the current `Tuple<...>` types. Member access would be compiled into access of members called `Item1` etc. The language level member names would be encoded in attributes for cross-assembly purposes: + +``` c# +struct VTuple2 { ... } +struct VTuple3 { ... } + +/* Source: */ +(x: int, y: int) f() { ... } + +/* Generates: */ +[TupleNames("x", "y")] +VTuple2 f() {...} +``` + +Unfortunately this leaves problems at runtime: now there's *too much* type equivalence. If I want to do a runtime test to see if an object is an `(int x, int y)`, I would get false positives for an `(int a, int b)`, since the names wouldn't be part of the runtime type: + +``` c# +object o = (a: 7, b: 9); +if (o is (int x, int y) t) WriteLine(t.x); // false positive +``` + +Also the member names would be lost to `dynamic`: + +``` c# +dynamic d = (a: 7, b: 9); +WriteLine(t.a); // Error! Huh ??!? +``` + + +Of course we could come up with tricks so that a tuple would carry some kind of member name info around at runtime, but not without cost. Or we could decide that the names are ephemeral, compile time only entities, kind of like parameter names, and losing them when boxing is actually a good thing. + +Also, this approach means that languages that don't know about tuples would see the actual `Item1` member names when referencing member signatures containing tuples. Such languages include previous versions of C#! So for compatibility with those versions of C#, even in the new, tuple-aware, version, access through `Item1` etc would have to be still legal (though probably hidden from IntelliSense etc.). + +A nice thing about this scheme is that it leads to very little code bloat: it only relies on framework types, and not on compiler generated types in the assembly. + +This remains an issue for further debate. + + +Conversions +----------- + +There are several kinds of conversions you can imagine allowing between tuple types: + +``` c# +(string name, int age) t = ("Johnny", 29); + +/* Covariance */ +(object name, int age) t1 = t; + +/* Truncation */ +(object name) t2 = t; + +/* Reordering */ +(int age, string name) t3 = t; + +/* Renaming */ +(string n, int a) t4 = t; +``` + +Note that you cannot have both reordering and renaming in the language; we would have to choose. + +However, we are more inclined not to allow *any* of these conversions. They don't seem particularly helpful, and are likely to mask something you did wrong. People can always deconstruct and reconstruct if they want to get to a different tuple type, so our default position is to be super rigid here and only match exactly the same names and types in exactly the same order. + + +Deconstruction +-------------- + +There are a couple of issues with the proposed deconstruction syntax. + +First of all, since the recipient variables are on the left, there is no equivalent to parameter help guiding you as to what to name them. In that sense it would be better to have a syntax that receives the values on the right. But that seems an odd place to declare new variables that are intended to be in scope throughout the current block: + +``` c# +Tally(myValues) ~> (var sum, var count); // strawman right side alternative +Console.WriteLine($"Sum: {sum}, count: {count}"); +``` + +That's not an actual proposed syntax, but just intended to show the point! + +A related issue is that you'd sometimes want to deconstruct into *existing* variables, not declare new ones: + +``` c# +(sum, count) = Tally(myValues); +``` + +Should this be allowed? Is deconstruction then a declaration statement, an assignment statement or something different? + +Finally, once we have a deconstruction syntax, we'd probably want to enable it for other things than tuples. Maybe types can declare how they are to be deconstructed, in a way that the language understands. So deconstruction syntax should be considered in this broader context. For instance, if deconstructable types can be reference types, can deconstruction throw a null reference exception? + + +LINQ +---- + +You could imagine tuples becoming quite popular in LINQ queries, to the point of competing with anonymous types: + +``` c# +from c in customers +select (name: c.Name, age: c.Age) into p +where p.name == "Kevin" +select p; +``` + +To this end it would be useful for tuple "literals" to support *projection*, i.e. inference of member names, like anonymous types do: + +``` c# +from c in customers +select (c.Name, c.Age) into p // infers (string Name, int Age) +where p.Name == "Kevin" +select p; +``` + + +Out parameters +-------------- + +F# allows out parameters to be seen as additional return values. We could consider something similar: + +``` c# +bool TryGet(out int value){ ... } + +/* current style */ +int value; +bool b = TryGet(out value); + +/* New style */ +(int value, bool b) = TryGet(); +``` + + +Conclusion +---------- + +We'd very much like to support tuples! There are a number of open questions, as well as interactions with other potential features. We'll want to keep debating and experimenting for a while before we lock down a design, to make sure we have the best overall story. \ No newline at end of file diff --git a/meetings/LDM-2015-03-04.md b/meetings/LDM-2015-03-04.md new file mode 100644 index 0000000000..40ef5396e9 --- /dev/null +++ b/meetings/LDM-2015-03-04.md @@ -0,0 +1,140 @@ +C# Design Meeting Notes for Mar 4, 2015 +======================================== + +Discussion on these notes can be found at https://github.com/dotnet/roslyn/issues/1303. + +Agenda +------ + +1. `InternalImplementationOnly` attribute <*no*> +2. Should `var x = nameof(x)` work? <*no*> +3. Record types with serialization, data binding, etc. <*keep thinking*> +4. "If I had a [billion dollars](http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare)...": nullability <*daunting but worth pursuing*> + + + +1. InternalImplementationOnly +============================= + +This was a last-minute proposal for C# 6 and VB 14 to recognize and enforce an attribute to the effect of disallowing non-internal implementation of an interface. It would be useful to represent "abstract hierarchies", while reserving the right to add new members at a later stage. + + +Conclusion +---------- + +Let's not do this. We cannot enforce it well enough since old compilers don't know about it. + + + +2. Should `var x = nameof(x)` work? +=================================== + +This was raised as issue #766. + +``` c# +var POST = nameof(POST); +``` + + +Conclusion +---------- + +This works the same as with any other construct, i.e.: not. This is not a special case for `nameof`, and it doesn't seem worth special casing to allow it. + + + +3. Records +========== + +There's general excitement around proposals such as #206 to add easier syntax for declaring "records", i.e. simple data shapes. + +This discussion is about the degree to which such a feature should accommodate interaction with databases, serialization and UI data binding. + +The problem has compounded in recent times. In a mobile app, pretty much everything has to be serialized one way or another. There's a big difference between e.g. JSON and binary serialization. In one you want readable property names (in a certain format), in the other you want compactness. ORMs are also essentially serialization. POCO was invented in that same context, but only gets you part of the way there. + +Clearly, tying the language feature to specific such technologies is not a sustainable idea. There will not be built-in JSON literals, or `INotifyPropertyChanged` implementation or anything like that. + +That said, It is worth thinking about what can be done *in general* to support such features better. + +One problem is that each specific serialization and data binding technology tends to have certain requirements of the shape of the data objects. Serialization frameworks may expect the fields to be mutable, or to have certain attributes on them. UIs may expect properties to notify on change. Such requirements make it hard to have a single language-sanctioned mechanism for specifying the data shapes. + +A separate problem may be the degree to which reflection is used by these technologies. It is commonly used to get at metadata such as member names, but is also sometimes used to set or get the actual values. It ends up being the case that both serialization and UI use the objects only in a "meta" fashion; the only part of the program that actually makes use of the data in a strongly typed way ends up being the business logic in between. + +So in effect, data objects need to both facilitate strongly typed business logic based on their property names and shapes (and often in hierarchies), *and* satisfy requirements that are specific to the serialization and UI environments they are used in. Cross-cutting aspects, one might say, that clash in the implementation of the data objects. + +There are several possible approaches, not necessarily mutually exclusive: + +**Write the data types manually**: Often what ends up happening today. Just grit your teeth and churn out those INotifyPropertyChanged implementations. + +**Separate objects**: Often the "best practice" is to have separate objects for serialization, logic, and presentation. But having to write the glue code between these is a big pain, and people often have to do at least some of it manually. + +**Non-reflective meta-level access model**: E.g. implement dictionary-like behavior. Prevents need for reflection, but is it actually better or faster? + +**"Fake" types**: Maybe at runtime the data objects shouldn't have strongly typed properties at all. Maybe the business logic is written up against a new kind of types that are only there at compile time, and which either simply erase like in TypeScript, or cause weakly typed but smart access code to be generated like with F# type providers. Then, from the perspective of serialization and UI, all the objects have the same type. + +**Compile-time metaprogramming**: Generate the types (either into source or IL) from code describing the data shapes, adding whatever serialization or presentation functionality is required. + +A particularly common requirement of these frameworks is for the data properties to be mutable. That might clash spectacularly with a records feature that tries to encourage immutability by default. + + +Conclusion +---------- + +We'll keep thinking about this situation. A next step in particular is to reach out to teams that own the serialization and presentation technologies that we'd like to work well with. + + + +Nullability and reference types +=============================== + +The horrifying thing about null being allowed as a member of reference types is that it does not obey the contract of those types. Reference types can be null *and* reference types can be dereferenced, But dereferencing null is a runtime error! No wonder Sir Tony famously calls null pointers his "[billion dollar mistake](http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare)". + +There are type systems that prevent null reference errors, and it is easy enough to do. It takes different shapes in different languages, but the core of it is that there is a non-nullable kind of reference type that can be dereferenced but cannot be null, and a nullable "wrapper" of that, which *can* be null, but cannot be dereferenced. The way you go from nullable to nonnullable is through a guard to test that the nullable value wasn't in fact null. Something like: + +``` c# +string? n; nullable string +string! s; non-nullable string + +n = null; // Sure; it's nullable +s = null; // Error! + +WriteLine(s.Length); // Sure; it can't be null so no harm +WriteLine(n.Length); // Error! +if (n is string! ns) WriteLine(ns.Length); // Sure; you checked and dug out the value +``` + +Of course, if such a type system were around from day one, you wouldn't need the `!` annotation; `string` would just *mean* non-nullable. + +It's a very common request (in fact [the top C# request on UserVoice](http://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c)) for us to do something about null reference exceptions along those lines. There are numerous obstacles, however: + +**Backward compatibility**: The cat is already very much out of the bag in that existing code needs to continue to compile without error - no matter how null-unsafe it is. The best we can do about *existing* code is to offer optional analyzers to point out dangerous places. + +**A default value for all types**: It is deeply ingrained in .NET and the CLR that every type has a default value. When you create an array with `new T[10]`, the array elements are initialized to the default value of `T`. There's even syntax to get the default value of a type, `default(T)`, and it is allowed even on unconstrained type parameters. But what is the default value of a non-nullable reference type? + +**Definite assignment of non-nullable fields is unenforceable**. Eric Lippert explains it well [on his blog](http://blog.coverity.com/2013/11/20/c-non-nullable-reference-types/). + +**Evolving libraries is breaking**: Say we added non-null annotations to the language. You'd then want to use them on your existing libraries to provide further guidance to your clients. Adding them on parameters would of course be breaking: + +``` c# +public void Foo(string! name) { ... } // '!' newly added + +Foo(GetString()); // Unless GetString also annotated, this is now broken +``` + +More subtly, adding the extra guarantee of non-nullability to a *return* type would also be breaking: + +``` c# +public string! Foo() { ... } // '!' newly added + +var s = Foo(); // Now infers 'string!' instead of 'string' for s +... +s = GetString(); // So this assignment is now broken +``` + +Type inference and overload resolution generally make strengthening of return types a potentially breaking change, and adding non-nullability is just a special case of that. + + +Conclusion +---------- + +Moving C# to a place of strong guarantees around nullability seems out of the question. But that is not to say that we cannot come up with an approach that meaningfully reduces the number of null reference exceptions. We want to keep digging here to see what can be done. The perfect is definitely the enemy of the good with this one. \ No newline at end of file diff --git a/meetings/LDM-2015-03-10,17.md b/meetings/LDM-2015-03-10,17.md new file mode 100644 index 0000000000..e2da37d1cd --- /dev/null +++ b/meetings/LDM-2015-03-10,17.md @@ -0,0 +1,170 @@ +C# Design Meeting Notes for Mar 10 and 17, 2015 +=============================================== + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/1648. + +Agenda +------ + +These two meetings looked exclusively at nullable/non-nullable reference types. I've written them up together to add more of the clarity of insight we had when the meetings were over, rather than represent the circuitous path we took to get there. + +1. Nullable and non-nullable reference types +2. Opt-in diagnostics +3. Representation +4. Potentially useful rules +5. Safely dereferencing nullable reference types +6. Generating null checks +1. Nullable and non-nullable reference types +============================================ + +The core features on the table are nullable and non-nullable reference types, as in `string?` and `string!` respectively. We might do one or both (or neither of course). + +The value of these annotations would be to allow a developer to express intent, and to get errors or warnings when working against that intent. + + + +2. Opt-in diagnostics +===================== + +However, depending on various design and implementation choices, some of these diagnostics would be a breaking change to add. In order to get the full value of the new feature but retain backward compatibility, we therefore probably need to allow the enforcement of some or most of these diagnostics to be *opt-in*. That is certainly an uncomfortable concept, and adding switches to the language changing its meaning is not something we have much of an appetite for. + +However, there are other ways of making diagnostics opt-in. We now have an infrastructure for custom analyzers (built on the Roslyn infrastructure). In principle, some or all of the diagnostics gained from using the nullability annotations could be custom diagnostics that you'd have to switch on. + +The downside of opt-in diagnostics is that we can forget any pretense to guarantees around nullability. The feature would help you find more errors, and maybe guide you in VS, but you wouldn't be able to automatically trust a `string!` to not be null. + +There's an important upside though, in that it would allow you to gradually strengthen your code to nullability checks, one project at a time. + + + +3. Representation +================= + +The representation of the annotations in metadata is a key decision point, because it affects the number of diagnostics that can be added to the language itself without it being a breaking change. There are essentially four options: + +1. Attributes: We'd have `string?` be represented as `string` plus an attribute saying it's nullable. This is similar to how we represent `dynamic` today, and for generic types etc. we'd use similar tricks to what we do for `dynamic` today. + +2. Wrapper structs: There'd be struct types `NullableRef` and `NonNullableRef` or something like that. The structs would have a single field containing the actual reference. + +3. Modreq's: These are annotations in metadata that cause an error from compilers that don't know about them. + +4. New expressiveness in IL: Something specific to denote these that only a new compiler can even read. + +We can probably dispense with 3 and 4. We've never used modreq's before, and who knows how existing compilers (of all .NET languages!) will react to them. Besides, they cannot be used on type arguments, so they don't have the right expressiveness. A truly new metadata annotation has similar problems with existing compilers, and also seems like overkill. + +Options 1 and 2 are interesting because they both have meaning to existing compilers. + +Say a library written in C# 7 offers this method: + +``` c# +public class C +{ + string? M(string! s) { ... } +} +``` + +With option 1, this would compile down to something like this: + +``` c# +public class C +{ + [Nullable] string M([NonNullable] string s) { ... } +} +``` + +A consuming program in C# 6 would not be constrained by those attributes, because the C# 6 compiler does not know about them. So this would be totally fine: + +``` c# +var l = C.M(null).Length; +``` + +Unfortunately, if something is fine in C# 6 it has to also be fine in C# 7. So C# 7 cannot have rules to prevent passing null to a nonnullable reference type, or prevent dereferencing a nullable reference type! + +That's obviously a pretty toothless - and hence useless - version of the nullability feature in and of itself, given that the value was supposed to be in getting diagnostics to prevent null reference exceptions! This is where the opt-in possibility comes in. Essentially, if we use an attribute encoding, we need all the diagnostics that make nullability annotations useful be opt-in, e.g. as custom diagnostics. + +With option 2, the library would compile down to this: + +``` c# +public class C +{ + NullableRef M(NonNullableRef s) { ... } +} +``` + +Now the C# 6 program above would not compile. The C# 6 compiler would see structs that can't be null and don't have a Length. Whatever members those structs *do* have, though, would be accessible, so C# 7 would still have to accept using them as structs. (We could mitigate this by not giving the structs any public members). + +For the most part, this approach would make the C# 6 program able to do so little with the API that C# 7, instead of adding *restrictions*, can allow *more* things than C# 6. + +There are exceptions, though. For instance, casting any returned such struct to `object` would box it in C# 6, whereas presumably the desired behavior in C# 7 would be to unwrap it. This is exactly where the CLR today has special behavior, boxing nullable value types by first unwrapping to the underlying type if possible. + +Also, having these single-field structs everywhere is likely going to have an impact on runtime performance, even if the JIT can optimize many of them away. + +Probably the most damning objection to the wrapper structs is probably the degree to which they would hamper interoperation between the different variations of a type. For instance, the conversion from `string!` to `string` and on to `string?` wouldn't be a reference conversion at runtime. Hence, `IEnumerable` wouldn't convert to `IEnumerable`, despite covariance. + +We are currently leaning strongly in the direction of an attribute-based representation, which means that there needs to be an opt-in mechanism for enforcement of the useful rules to kick in. + + + +4. Potentially useful rules to enforce +====================================== + +**Don't dereference `C?`**: you must check for null or assert that the value is not null. + +**Don't pass `null`, `C` or `C?` to `C!`:** you must check for null or assert that the value is not null. + +**Don't leave `C!` fields unassigned:** require definite assignment at the end of the constructor. (Doesn't prevent observing null during initialization) + +**Avoid `default(C!)`:** it would be null! + +**Don't instantiate `C![]`:** it's elements would be null. This seems like a draconian restriction - as long as you only ever read fields from the array that were previously written, no-one would observe the default value. Many data structures wrapping arrays observe this discipline. + +**Don't instantiate `G`:** this is because the above rules aren't currently enforced on even unconstrained type parameters, so they could be circumvented in generic types and methods. Again, this restriction seems draconian. No existing generic types could be used on nonnullable reference types. Maybe the generic types could opt in? + +**Don't null-check `C!`:** oftentimes using e.g. `?.` on something that's already non-nullable is redundant. However, since non-nullable reference types *can* be null, maybe flagging such checks is not always so helpful? + +We very much understand that these rules can't be perfect. The trade-off needs to be between adding value and allowing continuity with existing code. + + + +5. Safely dereferencing nullable reference types +================================================ + +For nullable reference types, the main useful error would come from dereferencing the value without checking for null. That would often be in the shape of the null-conditional operator: + +``` c# +string? s = ...; +var l = s?.Length; +var c = s?[3]; +``` + +However, just as often you'd want the null test to guard a block of code, wherein dereferencing is safe. An obvious candidate is to use pattern matching: + +``` c# +string? ns = ...; +if (ns is string! s) // introduces non-null variable s +{ + var l = s.Length; + var c = s[3]; +} +``` + +It is somewhat annoying to have to introduce a new variable name. However, in real code the expression being tested (`ns` in the above example) is more likely to be a more complex expression, not just a local variable. Or rather, the `is` expression is how you'd get a local variable for it in the first place. + +More annoying is having to state the type again in `ns is string! s`. We should think of some shorthand, like `ns is ! s` or `ns is var s` or something else. + +Whatever syntax we come up with here would be equally useful to nullable *value* types. + + + +6. Generating null checks for parameters +======================================== + +There'd be no guarantees that a `string!` parameter actually isn't going to be null. Most public API's would probably still want to check arguments for null at runtime. Should we help with that by automatically generating null checks for `C!` parameters? + +Every generated null check is performance overhead and IL bloat. So this may be a bad idea to do on every parameter with a non-nullable reference type. But we could have the user more compactly indicate the desire to do so. As a complete strawman syntax: + +``` c# +public void M(string!! s) { ... } +``` +Where the double `!!` means the type is non-nullable *and* a runtime check should be generated. + +If we choose to also do contracts (#119), it would be natural for this feature to simply be a shorthand for a null-checking `requires` contract. diff --git a/meetings/LDM-2015-03-18.md b/meetings/LDM-2015-03-18.md new file mode 100644 index 0000000000..d8fa33e006 --- /dev/null +++ b/meetings/LDM-2015-03-18.md @@ -0,0 +1,186 @@ +C# Design Meeting Notes for Mar 18, 2015 +======================================== + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/1677 + +Agenda +------ + +In this meeting we looked over the top [C# language feature requests on UserVoice](http://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c) to see which ones are reasonable to push on further in C# 7. + + + +1. Non-nullable reference types (*already working on them*) +2. Non-nullary constructor constraints (*require CLR support*) +3. Support for INotifyPropertyChanged (*too specific; metaprogramming?*) +4. GPU and DirectX support (*mostly library work; numeric constraints?*) +5. Extension properties and static members (*certainly interesting*) +6. More code analysis (*this is what Roslyn analyzers are for*) +7. Extension methods in instance members (*fair request, small*) +8. XML comments (*Not a language request*) +9. Unmanaged constraint (*requires CLR support*) +10. Compilable strings (*this is what nameof is for*) +11. Mulitple returns (*working on it, via tuples*) +12. ISupportInitialize (*too specific; hooks on object initializers?*) +13. ToNullable (*potentially part of nullability support*) +14. Statement lambdas in expression trees (*fair request, big feature!*) +15. Language support for Lists, Dictionaries and Tuples (*Fair; already working on tuples*) + +A number of these are already on the table. + + + +1. Non-nullable reference types +=============================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2320188-add-non-nullable-reference-types-in-c](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2320188-add-non-nullable-reference-types-in-c) + +We're already working on this; see e.g. #1648. + + + +2. Non-nullary constructor constraints +====================================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2122427-expand-generic-constraints-for-constructors](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2122427-expand-generic-constraints-for-constructors) + +It is odd that we only support the `new()` constraint for empty parameter lists. In order to generalize this, however, we'd need CLR support to express it - see #420. + + + +3. Support for INotifyPropertyChanged +===================================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2255378-inotifypropertychanged](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2255378-inotifypropertychanged) + +This is too specific an interface to bake in special knowledge for in the language. However, we recognize the pain of having to repeat the boilerplate around this, even as we've improved the situation here a bit with C# 6. + +We think that this may be better addressed with metaprogramming. While we don't have a clear story for how to support this better, in the language or compiler tool chain, we think that Roslyn in and of itself helps here. + +We'll keep watching the space and the specific scenario. + + + +4. GPU and DirectX support +========================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2730068-greatly-increase-support-for-gpu-programming-in-c](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2730068-greatly-increase-support-for-gpu-programming-in-c) +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3646222-enable-hlsl-directx-and-graphics-development-tool](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3646222-enable-hlsl-directx-and-graphics-development-tool) + +These are mostly library-level requests, independent of the language. + +One feature that could potentially improve such libraries would be the ability to specify generic constraints that somehow express the presence of numeric operators. Being able to write generic methods, say, over anything that has a `+` operator, allowing `+` to be used directly in that method body, would certainly improve the experience of writing such code, and would prevent a lot of repetition. + +Unfortunately, like other new constraints, such a numeric constraint facility would require new support from the CLR. + + + +5. Generalized extension members +================================ + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2242236-allow-extension-properties](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2242236-allow-extension-properties) +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2060313-c-support-static-extension-methods-like-f](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2060313-c-support-static-extension-methods-like-f) + +The requests for extension properties and for static extension methods are essentially special cases of a general desire to be able to more generally specify extension member versions of all kinds of function members: properties, indexers, constructors, static members - why not? + +This is a reasonable request. The main problem we have is that the current scheme for extension methods doesn't easily generalize to other kinds of members. We'd need to make some very clever syntactic tricks to do this without it feeling like a complete replacement of the current syntax. + +This is certainly one that we will look at further for C# 7. + + + +6. More code analysis +===================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4428274-improve-code-analysis](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4428274-improve-code-analysis) + +This feels like it is best addressed via Roslyn-based analyzers. + + + +7. Extension methods in non-static classes +========================================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3359397-allow-extension-methods-to-be-defined-in-instance](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3359397-allow-extension-methods-to-be-defined-in-instance) + +We were very cautious when we first introduced extension methods, and surrounded them with a lot of restrictions. This is a well argued scenario where allowing them inside instantiable classes would enable a fluent style for private helper methods. + +This seems fair enough, and we could loosen this restriction, though it's probably a relatively low priority work item. + + + +8. XML comments +=============== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2709987-xml-comments-schema-customization-in-c](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2709987-xml-comments-schema-customization-in-c) + +This is not a language suggestion. + + + +9. Unmanaged constraint +======================= + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4716089-unmanaged-generic-type-constraint-generic-pointe](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4716089-unmanaged-generic-type-constraint-generic-pointe) + +This would be great in order to enable pointers over type parameters. However, it requires CLR support. + + + +10. Compilable strings +====================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/5592955-compliable-strings](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/5592955-compliable-strings) + +This is mostly addressed by `nameof` in C# 6; it's unlikely there is basis for more language level functionality here. + + + +11. Mulitple returns +==================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2083753-return-multiple-values-from-functions-effortlessly](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2083753-return-multiple-values-from-functions-effortlessly) + +We're already looking at addressing this through tuples. + + + +12. ISupportInitialize +====================== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2094881-add-support-for-isupportinitialize-on-object-initi](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2094881-add-support-for-isupportinitialize-on-object-initi) + +This suggestion addresses the need to perform validation upon initialization. While depending on the ISupportInitialize interface is probably too specific, it is interesting to ponder if there is a way to e.g. hook in after an object initializer has run. + + + +13. ToNullable +============== + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2531917-structure-all-nullable-values](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2531917-structure-all-nullable-values) + +This suggestion would add a new operator to make type parameters nullable only if they are not already so. + +You could certainly imagine something like this in conjunction with at least some variations of the nullability proposals we have been discussing lately. + + + +14. Statement lambdas in expression trees +========================================= + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4255391-let-lambdas-with-a-statement-body-be-converted-to](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4255391-let-lambdas-with-a-statement-body-be-converted-to) + +The language today doesn't allow statement lambdas to be converted to expression trees, despite there being expression tree classes for most features. Moreover, in fact, it disallows several expression forms, including `await` expressions. + +This would be a lovely gap to fill, especially since it would come with no conceptual overhead - more code would just work as expected. However, it is also an enormous feature to take on. We are not sure we have the weight of scenarios necessary to justify taking on the full magnitude of this. + +It is also possible that we'd address a subset. + + + +15. Language support for Lists, Dictionaries and Tuples +======================================================= + +[http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2405699-build-list-dictionary-and-tuple-into-the-language](http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2405699-build-list-dictionary-and-tuple-into-the-language) + +We are already looking at tuples, and we do have it on our radar to consider language syntax for lists and dictionaries in some form. Many shapes of this feature would require a strong commitment to specific BCL types. However, you could also imagine having anonymous expression forms that target type to types that follow a given pattern. diff --git a/meetings/LDM-2015-03-24.md b/meetings/LDM-2015-03-24.md new file mode 100644 index 0000000000..550e33845c --- /dev/null +++ b/meetings/LDM-2015-03-24.md @@ -0,0 +1,100 @@ +C# Design Meeting Notes for Mar 24, 2015 +======================================== + +Discussion thread on these notes is at https://github.com/dotnet/roslyn/issues/1898. + +*Quote of the Day:* If we have slicing we also need dicing! + + +Agenda +------ + +In this meeting we went through a number of the performance and reliability features we have discussed, to get a better reading on which ones have legs. They end up falling roughly into three categories: + +* Green: interesting - let's keep looking +* Yellow: there's something there but this is not it +* Red: probably not + +As follows: + +1. ref returns and locals <*green*> (#118) +2. readonly locals and parameters <*green*> (#115) +3. Method contracts <*green*> (#119) +4. Does not return <*green*> (#1226) +5. Slicing <*green*> (#120) +6. Lambda capture lists <*yellow - maybe attributes on lambdas*> (#117) +7. Immutable types <*yellow in current form, but warrants more discussion*> (#159) +8. Destructible types <*yellow - fixing deterministic disposal is interesting*> (#161) +9. Move <*red*> (#160) +10. Exception contracts <*red*> +11. Static delegates <*red*> +12. Safe fixed-size buffers in structs <*red*> (#126) + +Some of these were discussed again (see below), some we just reiterated our position. + + + +1. Ref returns and locals +========================= + +At the implementation level these would require a verifier relaxation, which would cause problems when down targeting in sandboxing scenarios. This may be fine. + +At the language level, ref returns would have to be allowed on properties and indexers only if they do not have a setter. Setters and ref would be two alternative ways of allowing assignment through properties and indexers. For databinding scenarios we would need to check whether reflection would facilitate such assignment through a ref. + +A danger of ref returns in public APIs: say you return a ref into the underlying array of e.g. a list, and the list is grown by switching out the underlying array. Now someone can get a ref, modify the collection, follow the ref and get to the wrong place. So maybe ref returns are not a good thing on public API boundary. + +There's complexity around "safe to return": You should only return refs that you received as parameters, or got from the heap. This leads to complexity around allowing reassignment of ref locals: how do you track whether the ref they are pointing to is "safe to return" or not? We'd have to either + +* add syntax to declare which kind the local points to (complex) +* allow only one of the kinds to be assigned to locals (restrictive) +* track it as best we can through flow analysis (magic) + +There's complexity in how refs relate to readonly. You either can't take a ref to a readonly, or you need to be able to express a readonly ref through which assignments is illegal. The latter would need explicit representation in metadata, and ideally the verifier would enforce the difference. + +This can't very well be a C# only feature, at least if it shows up in public APIs. VB and F# would need to at least know about it. + +This feature would be a decent performance win for structs, but there aren't a lot of structs in the .NET ecosystem today. This is a chicken-and-egg thing: because structs need to be copied, they are often too expensive to use. So this feature could lower the cost of using structs, making them more attractive for their other benefits. + +Even so, we are still a bit concerned that the scenario is somewhat narrow for the complexity the feature adds. The proof will have to be in the use cases, and we're not entirely convinced about those. It would be wonderful to hear more from the community. This is also a great candidate for a prototype implementation, to allow folks to experiment with usability and performance. + + + +2. Readonly locals and parameters +================================= + +At the core this is a nice and useful feature. The only beef we have with it is that you sort of want to use `readonly` to keep your code safe, and you sort of don't because you're cluttering your code. The `readonly` keyword simply feels a bit too long, and it would be nice to have abbreviations at least in some places. + +For instance `readonly var` could be abbreviated to `val` or `let`. Probably `val` reads better than `let` in many places, e.g. declaration expressions. We could also allow `val` as an abbreviation for `readonly` even in non-var situations. + +In Swift they use `let` but it reads strange in some contexts. In Swift it's optional in parameter positions, which helps, but we couldn't have that for back compat reasons. + +This is promising and we want to keep looking at it. + + + +4. Does Not Return +================== + +It would be useful to be able to indicate that a method will never return successfully. It can throw or loop. + +The proposal is to do it as an attribute, but there would be more value if it was part of the type system. Essentially it replaces the return type, since nothing of that type is ever returned. We could call it `never`. The `never` type converts to pretty much anything. + +This would allow us to add throw *expressions* in the language - their type would be `never`. + +Having it in the type system allows e.g. returning `Task`, so that you can indicate an async method that will only ever produce an exception, if anything. + +Because of the Task example you do want to allow `never` in generics, but that means you could have generic types that unwittingly operate on never values, which is deeply strange. This needs to be thought about more. + +If through nasty tricks you get to a point in the code that according to never types should not be reachable, the code should probably throw. + +A common usage would be helper methods to throw exceptions. But throw as an expression is the most useful thing out of this. + + + +6. Attributes on lambdas +======================== + +Why? Guiding an analyzer, e.g. to prevent variable capture. Syntactically it might collide with XML literals in VB. + +We could probably hack it in. The attribute would be emitted onto the generated method. + diff --git a/meetings/LDM-2015-03-25 (Design Review).md b/meetings/LDM-2015-03-25 (Design Review).md new file mode 100644 index 0000000000..fd5039465c --- /dev/null +++ b/meetings/LDM-2015-03-25 (Design Review).md @@ -0,0 +1,173 @@ +C# Language Design Review, Mar 25, 2015 +======================================= + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/1921. + +We've recently changed gears a little on the C# design team. In order to keep a high design velocity, part of the design team meets one or two times each week to do detailed design work. Roughly monthly the full design team gets together to review and discuss the direction. This was the first such review. + + +Agenda +------ + +1. Overall direction +2. Nullability features +3. Performance and reliability features +4. Tuples +5. Records +6. Pattern matching +1. Overall direction +==================== + +In these first two months of design on C# 7 we've adopted a mix of deep dive and breadth scouring. There's agreement that we should be ambitious and try to solve hard problems, being willing to throw the result away if it's not up to snuff. We should keep an open mind for a while still, and not lock down too soon on a specific feature set or specific designs. + + + +2. Nullability features +======================= + +Non-nullable types are the number one request on UserVoice. We take it that the *underlying problem* is trying to avoid null reference exceptions. Non-nullable types are at best only part of the solution to this. We'd also need to help prevent access when something is *nullable*. + +We've looked at this over a couple of design meetings (#1303, #1648). Ideally we could introduce non-null types, such as `string!` that are guaranteed never to be null. However, the problems around initialization of fields and arrays, etc., simply run too deep. We can never get to full guarantees. + +We've been mostly looking at implementation approaches that use type erasure, and that seems like a promising approach. + +However, the thing we need to focus on more is this: when you get a nullability warning from this new feature, how do you satisfy the compiler? If you need to use unfamiliar new language features or significant extra syntax to do so, it probably detracts from the feature. + +Instead we should at least consider an flow-based approach, where the "null-state" of a variable is tracked based on tests, assignments etc. + +``` c# +if (x == null) return; +// not null here +``` + +It's an open question how far we would go. Would we track only locals and parameters, or would we also keep track of fields? + +``` c# +if (foo.x == null) ... +``` + +This is more problematic, not just because of the risk of other threads changing the field, but also because other code may have side effects on the field or property. + +TypeScript uses information about type guards to track union types in if branches, but it's not full flow analysis, and works only for local variables. Google Closure is more heuristics based, and is happy to track e.g. `foo.bar.baz` style patterns. + +A core nuisance with nullability checking is that it raises a wealth of compat questions that limit the design in different ways. There may need to be some sort of opt-in to at least some of the diagnostics you'd get, since you wouldn't want them if you were just recompiling old code that used to "work". + + + +3. Performance and reliability +============================== + +The list produced at a recent design meeting (#1898) looks sensible. + +val / readonly +-------------- + +We should cross check with Scala on their syntax. + + +ref return / locals +------------------- + +Lots of compexitity - we question whether it is worth the cost? + + +Never type +---------- + +The type system approach is interesting, and allows throw expressions. + +Method contracts +---------------- + +`requires` / `ensures`, show up in docs, etc. This looks great. The biggest question is what happens on failure: exceptions? fail fast? + + +Slices +------ + +We got strong feedback that array slices are only interesting if we unify them with arrays. Otherwise there's yet another bifurcation of the world. There's *some* value to have a `Slice` struct type just show up in the Framework. But it really doesn't seem worth it unless it's a runtime feature. That unification is really hard to achieve, and would require CLR support. It's valuable enough to try to pursue even with high likelihood of failure. + +Slicing as a language syntax could also be "overloadable" - on IEnumerables for instance. + +In Go, if you treat a slice as an object, it gets boxed. + + +Lambda capture lists +-------------------- + +Not interesting as a feature, but the idea of allowing attributes on lambdas might fly. + + +Immutable types +--------------- + +General concern that this doesn't go far enough, is lying to folks, etc. It tries to have strong guarantees that we can't make. + +But a lot of people would appreciate *something* here, so the scenario of immutability should continue to motivate us. + + +Destructible types +------------------ + +The scenario is good, not the current proposal. + + + +4. Tuple types +============== + +There's agreement on wanting the feature and on the syntax (#347, #1207). + +We probably prefer a value type version of Tuple. Of course those would be subject to tearing, like all structs. We're willing to be swayed. + +There are performance trade offs around allocation vs copying, and also around generic instantiation. We could do some experiments in F# source code, which already has tuples. + + + +5. Records +========== + +See #180, #206, #396, #1303, #1572. + +In the current proposal, we should just give up on the ability to name constructor parameters and members differently. The motivation was to be able to upgrade where parameter names start with lower case and member names upper case, but it's not worth the complexity. + +Should it have `==` and `!=` that are value based? Clashes a little with the ability to make them mutable. + +If I introduce extra state, then I have to write my own GetHashCode. That seems unfortunate. + +All the gunk today is part of why Roslyn uses XML to generate its data structure. A test of success would be for the Roslyn syntax trees to be concise to write in source code. + +A big issue here is incremental non-destructive modification. Roslyn follows the pattern of "Withers", a method for each property that takes a new value for that property and returns a new object that's a copy of the old one except for that property. Withers are painfully verbose to declare, and ideally this feature would offer a solution to that. + +Serialization has to work *somehow*, even though many of the members will be generated. + +We should not be *too* concerned about the ability to grow up to represent all kinds of things. Start from it being the POD feature, and work from there. + + + +6. Pattern matching +=================== + +See #180, #206, #1572. + +Whether introduced variables are mutable or not is not a key question: we can go with language uniformity or scenario expectation. + +Integral value matching is an opportunity to generalize. The pattern 3 may match the value 3 of all integral types rather than just the int 3. + +Named matching against all objects and positional against ones that define a positional match. Are recursive patterns necessary? No, but probably convenient and there's no reason not to have them. + +Pattern matching could be shoe-horned into current `switch` statements as well as `is` expressions. And we could have a switching *expression* syntax as well: + +``` c# +var x = match(e) { p => e, p => e, * => e } +``` +An expression version would need to be checked by the compiler for completeness. A little clunky, but much more concise than using a switch. + +Similar to a current pattern in the Roslyn code base: + +``` c# +Match(Func f1, Func f2) { ... } +var x = Match((string s) => e, (int i) => e); +``` + +Maybe the fat arrow is not right. We need to decide on syntax. Another option is to use the case keyword instead. diff --git a/meetings/LDM-2015-03-25.md b/meetings/LDM-2015-03-25.md new file mode 100644 index 0000000000..97547db186 --- /dev/null +++ b/meetings/LDM-2015-03-25.md @@ -0,0 +1,169 @@ +C# Design Meeting 2015-03-25 +============================ + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/1572. + +These are notes that were part of a presentation on 2015-03-25 of a snapshot of our design discussions for C# 7 and VB 15. The features under discussion are described in detail in #206 and elsewhere. + +[Records](https://github.com/dotnet/roslyn/issues/206) +======= + +Changes since Semih Okur's work: +- Remove `record` modifier +- `with` expressions (#5172) +- Working on serialization + +Working proposal resembles Scala case classes with active patterns. + +------ + +```cs +class Point(int X, int Y); +``` + +- defines a class (struct) and common members + - constructor + - `readonly` properties + - GetHashCode, Equals, ToString + - operator== and operator!= (?) + - Pattern-matching decomposition operator +- an association between ctor parameters and properties +- Any of these can be replaced by hand-written code +- Ctor-parameter and property can have distinct names + +------ + +### Use Cases +- Simplifies common scenarios +- Simple immutable user-defined data types + - Roslyn Syntax Trees, bound nodes +- Over-the-wire data +- Multiple return values + +------ + +### With expressions + +Illustrates an example of the value of having parameter-property association. + +Given + +```cs +struct Point(int X, int Y); +Point p = ... +``` + +the expression + +```cs +p with { Y = 4 } +``` + +is translated to + +```cs +new Point(p.X, 4) +``` + +------ + +### Open issues +- Closed hierarchy (and tag fields) +- Readonly +- Parameter names +- Can you opt out of part of the machinery? +- Serialization (in all of its forms) +- Construction and decomposition syntax + +------ + +[Pattern Matching](https://github.com/dotnet/roslyn/issues/206) +================ + +### Sources of Inspiration +- Scala +- F# +- Swift +- Rust +- Erlang +- Nemerle + +------ + +### A pattern-matching operation +- Matches a *value* with a *pattern* +- Either *succeeds* or *fails* +- Extracts selected values into *variables* + +```cs + object x = ...; + if (x is 3) ... + if (x is string s) ... + if (x is Point { X is 3, Y is int y }) ... + if (x is Point(3, int y)) ... +``` + +------ + +### Other aspects +- Patterns defined recursively +- "select" from among a set of pattern forms +- Typically an expression form +- *active patterns* support interop and user-defined types + +```cs +switch (o) +{ + case 3: + ... + break; + case string s: + M(s); + break; + case Point(3, int y): + M(y); + break; + case Point(int x, 4): + M(x); + break; +} +``` + +We think we want an expression form too (no proposed syntax yet, but for inspiration): + +```cs + M(match(e) { + 3 => x, + string s => s.foo, + Point(3, int y) => y, + * => null }) +``` + +------ + +### Benefits +- Condenses long sequences of complex logic with a test that resembles the shape of the thing tested + +### Use Cases +- Simplifies common scenarios +- Language manipulation code + - Roslyn + - Analyzers +- Protocol across a wire + +------ + +### Open questions +- How much of the pattern-matching experience do we want (if any)? +- Matching may be compiler-checked for completeness +- May be implemented using tags instead of type tests +- Which types have syntactic support? + - Primitives and string + - Records + - Nullable + - objects with properties + - anonymous types? + - arrays? + - List? Dictionary? + - Tuple<...>? + - IEnumerable? diff --git a/meetings/LDM-2015-04-01,08.md b/meetings/LDM-2015-04-01,08.md new file mode 100644 index 0000000000..edffd967a6 --- /dev/null +++ b/meetings/LDM-2015-04-01,08.md @@ -0,0 +1,138 @@ +C# Design Meeting Notes for Apr 1 and Apr 8, 2015 +================================================= + +Discussion thread for these notes is at https://github.com/dotnet/roslyn/issues/2119. + +Agenda +------ + +Matt Warren wrote a Roslyn analyzer as a low cost way to experiment with nullability semantics. In these two meetings we looked at evolving versions of this analyzer, and what they imply for language design. + +The analyzer is here: https://github.com/mattwar/nullaby. + + + +Flow-based nullability checking +=============================== + +At the design review on Mar 25 (#1921), there was strong support for adding nullability support to the language, but also the advice to make it easy to transition into using nullability checking by recognizing current patterns for null checking. + +This suggests a flow-based approach, where the "null state" of variables is tracked by the compiler, and may be different in different blocks of code. Comparisons of the variable, and assignments to the variable, would all change its null state within the scope of effect of those operations. + +An inherent danger with flow-based checks like that is that a variable may change in an untracked way. The risk of that for parameters and local variables is limited: the variable would have to be captured and modified by a lambda, and that lambda would have to be executed elsewhere *during* the running of the current function. Given that any null-checking machinery we build would have to be somewhat approximate anyway, we can probably live with this risk. + +It gets gradually worse if we try to track fields of `this` or other objects, or even properties or array elements. In all likelihood, tracking just parameters and local variables would deliver the bulk of the value, but at least "dotted chains" would certainly also be useful. + + + +Attributes versus syntax +======================== + +The analyzer makes use of attributes to denote when a variable is "nullable" (`[CouldBeNull]`) or "non-nullable" (`[ShouldNotBeNull]`). Compared to a built-in syntax, this has several disadvantages: + +* It's less syntactic convenient of course +* The attribute cannot be applied to local variables +* The attribute cannot be applied to a type argument or an array element type + +These limitations are inherent to the nature of the experiment, but we know how to counter them if we add language syntax, even if that syntax is encoded with the use of attributes. (We learned all the tricks when we introduced `dynamic`.) + + + +Analyzers versus built-in rules +=============================== + +Providing nullability diagnostics by means of an analyzer comes with a number of pros and cons compared to having those diagnostics built in to the language. + +* Language rules need to be clearly specified and reasonable to explain. An analyzer can employ more heuristics. +* Language-based diagnostics need to consider back compat for the language, whereas analyzers can introduce warnings on currently valid code. +* For the same reason, analyzers can evolve over time +* With an analyzer, individual rules can be turned on and off. Some may want a harsher medicine than others. With the language it has to be one size fits all. + +On the other hand: + +* Rules can probably be implemented more efficiently in the compiler itself (though we might be able to come up with tricks to deal with that for analyzers too) +* The language has an opportunity to standardize what exactly is allowed +* The rules would still apply in contexts where analyzers aren't run +* It would be odd to add `!` and `?` syntax to the language, without adding the accompanying semantics +* We could avoid many issues with back compat if we adopt the notion of "warning waves" (#1580), where later versions of the language can add new warnings. + +The analyzer has several shortcomings due to the lack of a public flow analysis API in Roslyn. That would be a great addition, regardless of what we do for nullability checking. + +With a cruder/simpler analysis built-in to the language, you can imagine folks building enhancement analyzers on top of it. Those may not just add new diagnostics, but might want to *turn off* compiler warnings where more heuristics can determine that they are in fact not warranted. The analyzer infrastructure doesn't currently support this. + + + +Taking the analyzer for a spin +============================== + +Given the following declaration + +``` c# +void Foo([ShouldNotBeNull] string s) { } +``` + +The following statements would yield warnings because `s` is declared to be nullable, but is used in a way that requires it not to be: + +``` c# +void Bad([CouldBeNull] string s) +{ + Foo(s); // Warning! + var l = s.Length; // Warning! +} +``` + +However, all the following methods are ok, because the flow analysis can determine that `s` is not null at the point where it is used: + +``` c# +void Ok1([CouldBeNull] string s) +{ + s = "Not null"; + Foo(s); // Ok +} +void Ok2([CouldBeNull] string s) +{ + if (s != null) + { + Foo(s); // Ok + } +} +void Ok3([CouldBeNull] string s) +{ + if (s == null) + { + throw new ArgumentNullException(); + } + Foo(s); // Ok +} +void Ok4([CouldBeNull] string s) +{ + if (s == null) + { + s = "NotNull"; + } + Foo(s); // Ok +} +void Ok5([CouldBeNull] string s) +{ + if (s != null && s.Length > 0) // Ok + { + } +} +void Ok6([CouldBeNull] string s) +{ + if (s == null || s.Length > 0) // Ok + { + } +} +``` + +This seems hugely useful, because current code will just continue to work in the vast majority of cases. + +It is a change of thinking from where nullability is strongly part of the *type* of a variable, and is established at declaration time. In that paradigm, establishing that a given variable is not null doesn't help you; you have to capture its value in a new, more strongly typed variable, which is more cumbersome, and which would require existing code to be extensively rewritten to match new patterns. + + + +Conclusion +========== + +Matt's experiment is great, and we are very interested in the nullability tracking approach, because it has the potential to make the bulk of existing code work in the face of new annotations. diff --git a/meetings/LDM-2015-04-14.md b/meetings/LDM-2015-04-14.md new file mode 100644 index 0000000000..11b8c1c14e --- /dev/null +++ b/meetings/LDM-2015-04-14.md @@ -0,0 +1,86 @@ +C# Design Meeting Notes for Apr 14 +================================== + +Discussion thread for these notes can be found at https://github.com/dotnet/roslyn/issues/2134. + +Bart De Smet visited from the Bing team to discuss their use of Expression Trees, and in particular the consequences of their current shortcomings. + +The Expression Tree API today is not able to represent all language features, and the language supports lambda conversions even for a smaller subset than that. + + + +Background +========== + +Certain Bing services, such as parts of Cortana, rely on shipping queries between different machines, both servers in the cloud and client devices. + +In a typical scenario a lambda expression tree is produced in one place, ideally via the conversion from lambda expressions to expression trees that exists in C#. The expression tree is then serialized using a custom serialization format, and sent over the wire. At the receiving end it will often be stitched into a larger expression tree, which is then compiled with the `Compile` method and executed. + +Along the way, several transformations are often made on the trees, for efficiency reasons etc. For instance, rather than invoke a lambda its body can often be inlined in the enclosing tree that the lambda gets stitched into. + +The serialization format is able to carry very specific type information along with the code, but can also represent the code loosely. A looser coupling makes for code that is more resilient to "schema" differences between the nodes, and also allows for use of types and functions that aren't present where the lambda is concocted. However, it also makes it harder to stich things back up right on the other side. + + + +Shortcomings in the Expression Tree API +======================================= + +The expression tree API was introduced with Linq and C# 3.0, and was extended to support the implementation infrastructure of `dynamic` along with C# 4.0. However, it has not been kept up to date with newer language features. + +This is a list of ones that are confounding to the Bing team. + + +Dynamic +------- + +Even though the API was extended for the benefit of the dynamic feature, the feature itself ironically is not well represented in the API. This is not a trivial undertaking: in Expression Trees today, all invoked members are represented using reflection structures such as `MemberInfo`s etc. Dynamic invocations would need a completely different representation, since they are by definition not bound by the time the expression tree is produced. + +In the Bing scenario, dynamic would probably help a lot with representing the loosely coupled invocations that the serialization format is able to represent. + +Alternatively, if the C# language added something along the "lightweight dynamic" lines that were discussed (but ultimately abandoned) for C# 6, the representation of that in Expression Trees would probably yield similar benefit with a fraction of the infrastructure. + + +Await +----- + +Representing await would probably be easy in the Expression Tree API. However, the implementation of it in `Expression.Compile` would be just as complex as it is in the C# and VB compilers today. So again, this is a significant investment, though one with much less public surface area complexity than `dynamic`. + +Needless to say, distributed scenarios such as that of Bing services have a lot of use for `await`, and it can be pretty painful to get along without it. + + +Null conditional operators and string interpolation +--------------------------------------------------- + +These should also be added as Expression Tree nodes, but could probably be represented as reducible nodes. Reducible nodes are a mechanism for describing the semantics of a node in terms of reduction to another expression tree, so that the `Compile` method or other consumers don't have to know about it directly. + + +Higher level statements +----------------------- + +While expression trees have support for statements, those tend to be pretty low level. Though there is a `loop` node there are no `for` or `foreach` nodes. If added, those again could be reducible nodes. + + + +Shortcomings in the languages +============================= + +C# and VB are even more limited in which lambdas can be converted to expression trees. While statements are part of the Expression Tree API, the languages will not convert them. Also, assignment operators will not be converted. + +This is a remnant of the first wave of Linq, which focused on allowing lambdas for simple, declarative queries, that could be translated to SQL. + +There has traditionally been an argument against adding more support to the languages based on the pressure this would put on existing Linq providers to support the new nodes that would start coming from Linq queries in C# and VB. + +This may or may not ever have been a very good argument. However, with Roslyn analyzers now in the world, it pretty much evaporates. Any Linq provider that wants to limit at design time the kinds of lambdas allowed, can write a diagnostic analyzer to check this, and ship it with their library. + +None of this support would require new syntax in the language, obviously. It is merely a matter of giving fewer errors when lambdas are converted to expression trees, and of mapping the additional features to the corresponding nodes in what is probably a straightforward fashion. + + + +Conclusion +========== + +There is no remaining design reason we can think of why we shouldn't a) bring the Expression Tree API up to date with the current state of the languages, and b) extend the language support accordingly. + +The main issue here is simply that it is likely to be a huge undertaking. We are not sure that the sum of the scenarios will warrant it, when you think about the opportunity cost for other language evolution. Bing is a very important user, but also quite probably an atypical one. + +So in summary, as a language design team, we certainly support completing the picture here. But with respect to priorities, we'd need to see a broader call for these improvements before putting them ahead of other things. diff --git a/meetings/LDM-2015-04-15.md b/meetings/LDM-2015-04-15.md new file mode 100644 index 0000000000..b0576d5e65 --- /dev/null +++ b/meetings/LDM-2015-04-15.md @@ -0,0 +1,72 @@ +C# Design Meeting Notes for Apr 15 +================================== + +Discussion thread for these notes is at https://github.com/dotnet/roslyn/issues/2133. + +Agenda +------ + +In this meeting we looked at nullability and generics. So far we have more challenges than solutions, and while we visited some of them, we don't have an overall approach worked out yet. + +1. Unconstrained generics +2. Overriding annotations +3. FirstOrDefault +4. TryGet + + +Unconstrained generics +====================== + +Fitting generics and nullability together is a delicate business. Let's look at unconstrained type parameters as they are today. One the one hand they allow access to members that are on all objects - `ToString()` and so on - which is bad for nullable type arguments. On the other hand they allow `default(T)` which is bad for non-nullable type arguments. + +Nevertheless it seems draconian to not allow instantiations of unconstrained type parameters with, say, `string?` and `string!`. We suspect that most generic types and methods probably behave pretty nicely in practice. + +For instance, `List`, `Dictionary` etc. would certainly have internal data structures - arrays - that would have the default value in several places. However, their logic would be written so that no array element that hadn't explicitly been assigned a value from the user would ever expose its default value later. So if we look at a `List` we wouldn't actually ever see nulls come out as a result of the internal array having leftover nulls from its initial allocation. No nulls would come in, and therefore no nulls would come out. We want to allow this. + + +Overriding annotations +====================== + +When type parameters are known to be reference types it may make sense to allow overriding of the nullability of the type parameter: `T?` would mean `sting?` regardless of whether the type argument was `string!`, `string` or `string?`. + +This would probably help in some scenarios, but with most generics the type parameter isn't actually known to be a reference type. + + +FirstOrDefault +============== + +This pattern explicitly returns a default value if the operation fails to find an actual value to return. Obviously that is unfortunate if the element type is non-nullable - you'd still get a null out! + +In practice, such methods tend to be so glaringly named (precisely because their behavior is a little funky) that most callers would probably already be wary of the danger. However, we might be able to do a little better. + +What if there was some annotation you could put on `T` to get the nullable version *if* `T` should happen to be a reference type. Maybe the situation is rare enough for this to just be an attribute, say `[NullableIfReference]`: + +``` c# +public [return:NullableIfReference] T FirstOrDefault(this IEnumerable src) +{ + if ... + else return default(T); +} +``` + +Applied to `List` (or `List`) this would return a `string?`. But applied to `List` it would return an `int` not an `int?`. + +This seems perfectly doable, but may not be worth the added complexity. + + +TryGet +====== + +This pattern has a bool return value signaling whether a value was there, and an out parameter for the result, which is explicitly supposed to be default(T) when there was no value to get: + +``` c# +public bool TryGet(out T result) { ... } +``` + +Of course no-one is expected to ever look at the out parameter if the method returns false, but even so it might be nice to do something about it. + +This is not a situation where we want to apply the `[NullableIfReference]` attribute from above. The consumer wants to be able to access the result without checking for null if they have already checked the returned bool! + +We could imagine another attribute, `[NullableIfFalse]` that would tell the compiler at the consuming site to track nullability based on what was returned from the method, just as if there had been a null check directly in the code. + +Again, this might not be worth the trouble but is probably doable. diff --git a/meetings/LDM-2015-04-22 (Design Review).md b/meetings/LDM-2015-04-22 (Design Review).md new file mode 100644 index 0000000000..fc9b7a1947 --- /dev/null +++ b/meetings/LDM-2015-04-22 (Design Review).md @@ -0,0 +1,61 @@ +# C# Language Design Review, Apr 22, 2015 + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/3910. + +## Agenda + +See #1921 for an explanation of design reviews and how they differ from design meetings. + +1. Expression tree extension +2. Nullable reference types +3. Facilitating wire formats +4. Bucketing + + +# Expression Trees + +Expression trees are currently lagging behind the languages in terms of expressiveness. A full scale upgrade seems like an incredibly big investment, and doesn't seem worth the effort. For instance, implementing `dynamic` and `async` faithfully in expression trees would be daunting. + +However, supporting `?.` and string interpolation seems doable even without introducing new kinds of nodes in the expression tree library. We should consider making this work. + + +# Nullable reference types + +A big question facing us is the "two-type" versus the "three-type" approach: We want you to guard member access etc. behind null checks when values are meant to be null, and to prevent you from sticking or leaving null in variables that are not meant to be null. In the "three-type" approach, both "meant to be null" and "not meant to be null" are expressed as new type annotations (`T?` and `T!` respectively) and the existing syntax (`T`) takes on a legacy "unsafe" status. This is great for compatibility, but means that the existing syntax is unhelpful, and you'd only get full benefit of the nullability checking by completely rooting out its use and putting annotations everywhere. + +The "two-type" approach still adds "meant to be null" annotations (`T?`), but holds that since you can now express when things *are* meant to be null, you should only use the existing syntax (`T`) when things are *not* meant to be null. This certainly leads to a simpler end result, and also means that you get full benefit of the feature immediately in the form of warnings on all existing unsafe null behavior! Therein of course also lies the problem with the "two-type" approach: in its simplest form it changes the meaning of unannotated `T` in a massively breaking way. + +We think that the "three-type" approach is not very helpful, leads to massively rewritten over-adorned code, and is essentially not viable. The "two-type" approach seems desirable if there is an explicit step to opt in to the enforcement of "not meant to be null" on ordinary reference types. You can continue to use C# as it is, and you can even start to add `?` to types to force null checks. Then when you feel ready you can switch on the additional checks to prevent null from making it into reference types without '?'. This may lead to warnings that you can then either fix by adding further `?`s or by putting non-null values into the given variable, depending on your intent. + +There are additional compatibility questions around evolution of libraries, but those are somewhat orthogonal: Maybe a library carries an assembly-level attribute saying it has "opted in", and that its unannotated types should be considered non-null. + +There are still open design questions around generics and library compat. + + +# Wire formats + +We should focus attention on making it easier to work with wire formats such as JSON, and in particular on how to support strongly typed logic over them without forcing them to be deserialized to strongly typed objects at runtime. Such deserialization is brittle, lossy and clunky as formats evolve out of sync, and extra members e.g. aren't kept and reserialized on the other end. + +There's a range of directions we could take here. Assuming there are dictionary-style objects representing the JSON (or other wire data) in a weakly typed way, options include: + +* Somehow supporting runtime conversions from such dictionaries to interfaces (and back) +* Compile-time only "types" a la TypeScript, which translate member access etc. to a well-known dictionary pattern +* Compile-time type providers a la F#, that allow custom specification not only of the compile-time types but also the code generated for access. + +We'd need to think about construction, not just consumption. + +``` c# +var thing = new Thing { name = "...", price = 123.45 } +``` + +Maybe `Thing` is an interface with an attribute on it: + +``` c# +[Json] interface { string name; double price; } +``` + +Or maybe it is something else. This warrants further exploration; the right feature design here could be an extremely valuable tool for developers talking to wire formats - and who isn't? + +# Bucketing + +We affirmed that the bucketing in issue #2136 reflects our priorities. \ No newline at end of file diff --git a/meetings/LDM-2015-05-20.md b/meetings/LDM-2015-05-20.md new file mode 100644 index 0000000000..163504bc36 --- /dev/null +++ b/meetings/LDM-2015-05-20.md @@ -0,0 +1,261 @@ +C# Design Meeting Notes for May 20 +================================== + +Discussion for this issue can be found at https://github.com/dotnet/roslyn/issues/3911. + +_Quote of the day:_ The slippery slope you are talking about is that if we satisfy our customers they'll want us to satisfy them some more. + + +Agenda +------ + +Today we discussed whether and how to add local functions to C#, with the aim of prototyping the feature in the near future. Proposal at issue #259. Some details are discussed in issue #2930. + +1. Local functions + + + +Local Functions +=============== + +We agree that the scenario is useful. You want a helper function. You are only using it from within a single function, and it likely uses variables and type parameters that are in scope in that containing function. On the other hand, unlike a lambda you don't need it as a first class object, so you don't care to give it a delegate type and allocate an actual delegate object. Also you may want it to be recursive or generic, or to implement it as an iterator. + + +Lambdas or local functions? +--------------------------- + +There are two ways of approaching it: + +* Make lambdas work for this +* Add local function syntax to the language + +On the face of it, this seems like it would be better to reuse an existing feature. After all, we are not looking to address a new problem but just make existing code more convenient to write. In VB this scenario works pretty nicely with lambdas already - can't we just take a page out of VB's book? + +Well, it turns out you need a plethora of little features to achieve the full effect with lambdas: + +* lambdas would need to have an intrinsic (compiler-generated) delegate type that we can infer for them when they are not target typed to a specific delegate type: + +``` c# +var f = (int x) => x * x; // infers a compiler generated delegate type for int -> int. +``` + +* lambdas need to be able to recursively call themselves through a variable name they get assigned to. This introduces a problem with inferring a return type: + +``` c# +var f = (int x, int c) => c = 0 ? x : f(x) * x; // can we reasonably infer a return type here? +var f = (int x, int c) => (int)(c = 0 ? x : f(x) * x); // or do we need to cast the result? +``` + +* lambdas need to be able to be iterators. Since they don't have a specified return type, how can we know if the iterator is supposed to be `IEnumerable`, `IEnumerator`, `IEnumerable` or `IEnumerator`? + +``` c# +var f = (int x, int c) => { for (int i = 0; i < c; i++) { yield return x; } }; // default to IEnumerable? +``` + +* We'd want lambdas to be generic. What's the syntax for a generic lambda - where do the type parameters go? Presumably in front of the parameter list? And wait a minute, we don't even have a notion of delegate types for generic functions! + +``` c# +var f = (IEnumerable src) => src.FirstOrDefault(); // Is this unambiguous? What's the delegate type? +``` + +VB does a subset of these, probably enough to get by, but all in all for C# it seems both the better and easier path to simply let you define a function in method scope. + +On top of this is the performance aspect: the lambda approach implies a lot of allocations: one for the delegate and one for the closure object to capture surrounding variables and type parameters. + +Sometimes one or both of these can be optimized away by a clever compiler. But with functions, the delegate is never there (unless you explicitly decide to create one when you need it), and if the function itself is not captured as a delegate, the closure can be a struct on the stack. + +**Conclusion**: Local functions are the better choice. Let's try to design them. + + +Syntax +------ + +The syntax of a local functions is exactly as with a method, except that it doesn't allow the syntactic elements that are concerned with it being a member. More specifically: + +* No attributes +* No modifiers except `async` and `unsafe` +* The name is always just an identifier (not `Interface.MemberName`) +* The body is never just `;` (always `=> ...` or `{ ... }`) + +We'll consider *local-function-declaration* to be a third kind of *declaration-statement*, next to *local-variable-declaration* and *local-constant-declaration*. Thus it can appear as a statement at the top level of a block, but cannot in itself be e.g. a branch of an if statement or the body of a while statement. + +Local functions need to be reconciled with top level functions in script syntax, so that they work as similarly as possible. Nested local functions in blocks, just like nested local variables, would truly be local functions even in script. + + +Examples +-------- + +A classical example is that of doing argument validation in an iterator function. Because the body of an iterator method is executed lazily, a wrapper function needs to do the argument checking. The actual iterator function can now be a local function, and capture all the arguments to the wrapper function: + +``` c# +public static IEnumerable Filter(IEnumerable source, Func predicate) +{ + if (source == null) throw new ArgumentNullException(nameof(source)); + if (predicate == null) throw new ArgumentNullException(nameof(predicate)); + + IEnumerable Iterator() + { + foreach (var element in source) + if (predicate(element)) + yield return element; + } + return Iterator(); +} + +``` + +An example of a recursive local function would be a Quicksort, for instance: + +``` c# +public static void Quicksort(T[] elements) where T : IComparable +{ + void Sort(int start, int end) + { + int i = start, j = end; + var pivot = elements[(start + end) / 2]; + + while (i <= j) + { + while (elements[i].CompareTo(pivot) < 0) i++; + while (elements[j].CompareTo(pivot) > 0) j--; + if (i <= j) + { + T tmp = elements[i]; + elements[i] = elements[j]; + elements[j] = tmp; + i++; + j--; + } + } + if (start < j) Sort(elements, start, j); + if (i < end) Sort(elements, i, end); + } + + Sort(elements, 0, elements.Length - 1); +} +``` + +Again it captures parameters and type parameters of the enclosing method, while calling itself recursively. + +For optimization purposes, some async methods are implemented with a "fast path" where they don't allocate a state machine or a resulting `Task` unless they discover that it's necessary. These aren't themselves `async`, but can have a nested local async function that they call when necessary, returning the resulting `Task`. Something along the lines of: + +``` c# +public Task GetByteAsync() +{ + async Task ActuallyGetByteAsync() + { + await buffer.GetMoreBytesAsync(); + byte result; + if (!buffer.TryGetBufferedByte(out result)) throw ...; // we just got more + return result; + } + + byte result; + if (!buffer.TryGetBufferedByte(out result)) return ActuallyGetByteAsync(); // slow path + + if (taskCache[result] == null) taskCache[result] = Task.FromResult(result); + return taskCache[result]; +} +``` + +By the way, we just did `taskCache[result]` three times there in the last two lines, each with its own bounds check. We could maybe optimize a little with a local function taking a ref: + +``` c# + Task GetTask(ref Task cache) + { + if (cache == null) cache = Task.FromResult(result); + return cache; + } + return GetTask(ref taskCache[result]); // only indexing once +``` + +Of course if we *also* add ref locals to the language, such trickery would not be necessary. + + +Scope and overloading +--------------------- + +In various ways we need to choose whether local functions are more like methods or more like local variables: + +* Local variables only allow one of a given name, whereas methods can be overloaded +* Local variables shadow anything of the same name in outer scopes, whereas method lookup will keep looking for applicable methods +* Local variables are not visible (it is an error to use them) before their declaration occurs, whereas methods are. + +There are probably scenarios where it would be useful to overload local functions (or at least more elegant not to have to give them all separate names). You can also imagine wanting to augment a set of existing overloads with a few local ones. + +However, it is somewhat problematic to allow local functions to be visible before their declaration: they can capture local variables, and it would provide an easy loophole for manipulating those variables before their declaration: + +``` c# +f(3); +int x; // x is now 3! +void f(int v) { x = v; } +``` + +Such pre-declaration manipulation is certainly possible today, but it requires more sneaky use of lambdas or goto statements, e.g.: + +``` c# + goto Assign; +Before: + goto Read; +Declare: + int x = 5; + goto Read; +Assign: + x = 3; + goto Before; +Read: + WriteLine(x); + if (x == 3) goto Declare; +``` + +This prints 3 and 5. Yeah, yuck. We should think twice before we allow local functions to make this kind of thing so much easier that you might likely do it by mistake. + +On the other hand, there's no nice way to write mutually recursive local functions, unless the first can also see the second. (The workaround would be to nest one within the other, but that's horrible). + +For now, for prototyping purposes, we'll say that local functions are more like local variables: there can only be one of a given name, it shadows same-named things in outer scopes, and it cannot be called from a source location that precedes its declaration. We can consider loosening this later. + +Definite assignment analysis of locals captured by local functions should be similar to lambdas for now. We can think of refining it later. + + +Type inference +-------------- + +Lambdas have a mechanism for inferring their return type. It is used for instance when a lambda is passed to a generic method, as part of that generic method's type inference algorithm. + +We could allow local functions to be declared with `var` as their return type. Just as `var` on local variables doesn't always succeed, we could fail whenever finding the return type of the local function is too hard; e.g. if it is recursive or an iterator. + +``` c# +var fullName(Person p) => $"{p.First} {p.Last}"; // yeah, it's probably going to be a string. +``` + +We don't need this for prototyping, but it is nice to consider for the final design of the feature. + + +Declaration expressions +----------------------- + +One thing lambdas have going for them is that they can be embedded in expression contexts, whereas local declarations currently can't, though we had a proposal for declaration expressions in C# 6. If we were to do some sort of let expressions, local functions should ideally work alongside local variables. The proposed C# 6 scheme would work for that: + +``` c# +return (IEnumerable Impl() { ... yield return ... }; Impl()); // Or no semicolon? +``` + +Why would you do that? For encapsulation, but especially if you're in a big expression context where pulling it out is too far. Imagine a string-interpolated JSON literal that has an array inside it that I want to construct with an iterator: + +``` c# +return $@"""hello"": ""world"" + ... // 30 lines of other stuff + ""list"" : { ( + IEnumerable GetValues() { + ... yield return ... + }; + JsonConvert.SerializeObject(GetValues())) }"; +``` + +We don't have to think about this now, but it is nice to know that it's possible if we ever do declaration expressions to include local functions as one of the things you can declare in an expression context. + + +Slippery slope +-------------- + +Local classes might be the next ask, but we perceive them as much less frequent, and other features we're discussing would make them even less so. diff --git a/meetings/LDM-2015-05-25.md b/meetings/LDM-2015-05-25.md new file mode 100644 index 0000000000..2e73fe6026 --- /dev/null +++ b/meetings/LDM-2015-05-25.md @@ -0,0 +1,54 @@ +# C# Design Meeting Notes for May 25, 2015 + +Discussion for these notes can be found in https://github.com/dotnet/roslyn/issues/3912. + +## Agenda + +Today we went through a bunch of the proposals on GitHub and triaged them for our list of features in issue #2136. Due to the vastness of the list, we needed to use *some* criterion to sort by, and though far from ideal we ordered them by number of comments, most to least. + +Here's where we landed, skipping things that were already "Strong interest". Some are further elaborated in sections below. + +1. Method contracts <*Stay at "Some interest"*>(#119) +2. Destructible types <*Stay at "Probably never"*> (#161) +3. Params IEnumerable <*Stay at "Small but Useful*>(#36) +4. Multiple returns <*Addressed by tuples. Close.*> (#102) +5. More type inference <*Not a coherent proposal. Close*> (#17) +6. Readonly parameters and locals <*Stay at "Some interest"*>(#115) +7. Implicitly typed lambdas <*Add at "Probably never"*>(#14) +8. Immutable types <*Stay at "Some interest*>(#159) +9. Object initializers for immutable objects <*Add at "Some interest"*>(#229) +10. First item is special <*Add at "Never"*>(#131) +11. Array slices <*Keep at "Interesting but needs CLR support"*>(#120) +12. Vararg calling convention <*Merge with params IEnumerable*>(#37) +13. XML Literals <*Add to "Never"*>(#1746) +14. Local Functions <*Move to "Some interest*>(#259) +15. Covariant returns <*Stay at "Some interest*>(#357) + +# Params IEnumerable + +This needs more thinking - let's not just implement the straightforward design. There are perf issues, for instance, around implementing through the `IEnumerable` interface instead of arrays directly. + +# More type inference + +Not a coherent proposal. But even if there was one, we probably wouldn't want it in C#. + +# Implicitly typed lambdas + +These are mostly subsumed by local functions, which we'd rather do. It has some individual usefulness but not much synergy. + +# Object initializers for immutable objects + +We want to think this together with withers, not sure what form it would take. + +# first item in loops is special + +We recognize the scenario but it's not worthy of a feature. + +# vararg calling convention + +Roll it in with params IEnumerable discussion for investigation. + +# XML literals + +Never! We won't bake in a specific format. + diff --git a/meetings/LDM-2015-07-01.md b/meetings/LDM-2015-07-01.md new file mode 100644 index 0000000000..e685de47ba --- /dev/null +++ b/meetings/LDM-2015-07-01.md @@ -0,0 +1,107 @@ +# C# Design Meeting Notes for Jul 1, 2015 + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/3913. + +## Agenda + +We are gearing up to prototype the tuple feature, and put some stakes in the ground for its initial design. This doesn't mean that the final design will be this way, and some choices are arbitrary. We merely want to get to where a prototype can be shared with a broader group of C# users, so that we can gather feedback and learn from it. + +# Tuples + +The tuples proposal #347 is pretty close to what we want, but has some open questions and unaddressed aspects. + +## Names + +We want the elements of tuples to be able to have names. It is exceedingly useful to be able to describe which elements have which meaning, and to be able to dot into a tuple with those names. + +However, it is probably not useful to make the names too strongly a part of the type of a tuple. There is no really good reason to consider (int x, int y) an fundamentally different tuple type than (int a, int b). In fact, according to the analogy with parameter lists, the names should be of secondary importance: useful for getting at the elements, yes, but just as parameter lists with different parameter names can overwrite each other, as long as the types match at the given parameter positions, so should tuple types be considered equivalent when they have the same types at the same positions. + +Another way to view it is that all tuples with the same types at the same positions share a common underlying type. We will make that type denotable in the language, in that you can write anonymous tuple types, `(int, int)`, even though you cannot write an anonymous parameter list. + +For now we don't think we will allow partially named tuples: it is either all names or none. + +``` c# +(int x, int y) t1 = ...; +(int a, int b) t2 = t1; // identity conversion +(int, int) t3 = t1; // identity conversion +``` + +All "namings" of a tuple type are considered equivalent, and are convertible to each other via an identity conversion. For type inference purposes, an inferred tuple type will have the names if all "candidate types" with names agree on them, otherwise it will be unnamed. + +``` c# +var a1 = new[]{ t1, t1 }; // infers (int x, int y)[], since all agree +var a2 = new[]{ t1, t2 }; // infers (int, int)[], since not all agree +var a3 = new[]{ t1, t3 }; // infers (int x, int y)[] since all with names agree +``` + +For method overriding purposes, a tuple type in a parameter or return position can override a differently named tuple type. The rule for which names apply at the call site are the same as those used for named arguments: the names from the most specific statically known overload. + +Tuple literals likewise come in named an unnamed versions. They can be target typed, but sometimes have a type of their own: + +``` c# +var t1 = ("Hello", "World"); // infers (string, string) +var t2 = (first: "John", last: "Doe"); // infers (string first, string last) +var t3 = ("Hello", null); // fails to infer because null doesn't have a type +var t4 = (first: "John", last: null); // fails to infer because null doesn't have a type + +(string, string) t5 = ("Hello", null); // target typed to (string, string) +(string first, string last) t6 = ("John", null); // target typed to (string first, string last) +(string first, string last) t7 = (first: "John", second: "Doe"); // error: when given, names must match up +(string first, string last) t8 = (last: "Doe", first: "John"); // fine, values assigned according to name +``` + +The last two are probably the only possibly controversial examples. When target typing with names in the literal, this seems very similar to using named arguments for a method call. These rules match that most closely. + +This is something we may want to return to, as it has some strange consequences. For instance, if we introduce a temporary variable for the tuple, and do not use target typing: + +``` c# +var tmp = (last: "Doe", first: "John"); // infers (string last, string first) +(string first, string last) t8 = tmp; // assigns by position, so first = "Doe" +``` + +But for now, let's try these rules out and see what they feel like. + +## Encoding + +Are core question is what IL we generate for tuples. The equivalence-across-names semantics make it easy to rely on a fixed set of underlying generic library types. + +We want tuples to be value types, since we expect them to be created often and copied less often, so it's probably worthwhile avoiding the GC overhead. + +Strangely perhaps, we want tuples to be mutable. We think that there are valid scenarios where you want to keep some data in a tuple, but be able to modify parts of it independently without overwriting the whole tuple. Also, calling methods on readonly tuple members that are themselves value types, would cause those methods to be called on a throw-away copy. This is way too expensive - it means that there may be no non-copying way of doing e.g. value-based equality on such tuples. + +So we encode each tuple arity as a generic struct with mutable fields. It would be very similar to the current Tuple<...> types in the BCL, and we would actively avoid purely accidental differences. For this reason, the fields will be called `Item1` etc. Also, tuples bigger than a certain arity (probably 8) will be encoded using nesting through a field called `Rest`. + +So we are looking at types like this: + +``` c# +public struct ValueTuple +{ + public T1 Item1; + public T2 Item2; + public T3 Item3; +} +``` + +Possibly with a constructor, some `Equals` and `GetHashCode` overrides and maybe an implementation of `IEquatable<...>` and `IStructurallyEquatable<...>`, but at its core exceedingly simple. + +In metadata, a named tuple is represented with its corresponding `ValueTuple<...>` type, plus an attribute describing the names for each position. The attribute needs to be designed to also be able to represent names in nested tuple types. + +The encoding means that an earlier version of C#, as well as other languages, will see the tuple members as `Item1` etc. In order to avoid breaking changes, we should probably keep recognizing those names as an alternative way of getting at any tuple's elements. To avoid confusion we should disallow `Item1` etc. as names in named tuples - except, perhaps, if they occur in their right position. + +## Deconstruction syntax + +Most tuple features have a deconstruction syntax along the following lines: + +``` c# +(int sum, int count) = Tally(...); +``` + +If we want such a syntax there are a number of questions to ask, such as can you deconstruct into existing fields or only newly declared ones? + +For now we sidestep the question by not adding a deconstruction syntax. The names make access much easier. If it turns out to be painful, we can reconsider. + +## Other issues + +We will not consider tuples of arity zero or one - at least for now. They may be useful, especially from the perspective of generated source code, but they also come with a lot of questions. + +It seems reasonable to consider other conversions, for instance implicit covariant conversions between tuples, but this too we will let lie for now. diff --git a/meetings/LDM-2015-07-07.md b/meetings/LDM-2015-07-07.md new file mode 100644 index 0000000000..b8a74a4722 --- /dev/null +++ b/meetings/LDM-2015-07-07.md @@ -0,0 +1,71 @@ +# C# Design Notes for Jul 7 2015 + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5031. + +Quotes of the day: + +> "I don't think I've had a Quote of the Day for years " +> "What you just described is awful, so we can't go there!" + +## Agenda + +With Eric Lippert from Coverity as an honored guest, we looked further at the nullability feature. + +1. Adding new warnings +2. Generics and nullability + +# New warnings + +The very point of this feature is to give new warnings about existing bugs. What's a reasonable experience here? Ideally folks will always be happy to be told more about the quality of the code. Of course there needs to be super straightforward ways of silencing warnings for people who just need to continue to build, especially when they treat warnings as errors (since those would be breaking). We've previously been burned by providing new warnings that then broke people. + +There's a problem when those warnings are false positives, which though hopefully rare, is going to happen: I checked for null in a way the compiler doesn't recognize. Can I build an analyzer to turn warnings *off* if it knows better? Anti-warnings? That's a more general analyzer design question. + +Coverity for instance have very advanced analysis to weed out false positives, and avoid the analysis becoming noisy. + +# Generics + +There's a proposal to treat generics in the following way: Both nullable and nonnullable types are allowed as type arguments. Nullability flows with the type. Type inference propagates nullability - if any input type is nullable the inferred type will be, too. + +Constraints can be nullable or nonnullable. Any nonnullable constraints mean that only non-nullable reference types can satisfy them (without warning). Unconstrained generics is reinterpreted to mean constrained by `object?`, in order to continue to allow all safe types as type arguments. If a type parameter `T` is constrained to be nonnullable, `T?` can be used without warning. + +Thus: + +``` c# +class C where T : Animal? +{ + public T x; + public T? y; // warning +} +class D where T : Animal +{ + public T x; + public T? y; +} + +C +C + +D +D // warning +``` + +Inside of generics, type parameters are always expected to possibly be nonnullable. Therefore assigning null or nullable values to them always yields a warning (except probably when they are from the *same* possibly nullable type parameter!). + +This is nice and consistent. Unfortunately it isn't quite expressive enough. There are cases such as `FirstOrDefault` where we'd really want to return "the nullable version of T if it isn't already nullable". Assume an operator "`^`" (that shouldn't *actually* be the syntax) to mean take the nullable of any nonnullable reference type, leave it alone otherwise: + +``` c# +public static T^ FirstOrDefault(this IEnumerable source); + +var a = new string[] { "a", "b", "c" }; +var b = new string[] {}; + +var couldBeNull = condition ? a.FirstOrDefault() : b.FirstOrDefault(); // string? +``` + +We would need to decide what the syntax for `^` *actually* is if we want to have that expressiveness. + +`T^` can also serve the purpose of helping null-check code *inside* of a generic method or type. A `List` type for instance could declare its storage array to be `T^[]` instead of `T[]` so that it warns you where you use it that the array elements could be null. + +`T?` cannot exist when T is not constrained to either struct or class, because it means different things in the two cases, and we can't code gen across them. + +Cast doesn't do null check, just suppresses warning. diff --git a/meetings/LDM-2015-08-18.md b/meetings/LDM-2015-08-18.md new file mode 100644 index 0000000000..1abcfcc867 --- /dev/null +++ b/meetings/LDM-2015-08-18.md @@ -0,0 +1,65 @@ +# C# Design Notes for Aug 18, 2015 + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5033. + +## Agenda + +A summary of the design we (roughly) landed on in #5031 was put out on GitHub as #5032, and this meeting further discussed it. + +1. Array creation +2. Null checking operator +3. Generics + +# Array creation with non-nullable types + +For array creation there is the question whether to allow (big hole) or disallow (big nuisance?) on non-nullable referance types. We'll leave it at allow for now, but may reconsider. + +# Null checking operator + +Casting to a non-nullable reference type would not, and should not, do a runtime null check. Should we, however, have an *operator* for checking null, throwing if the value *is* null, resulting in the non-null value if it isn't? + +This seems like a good idea. The operator is postfix `!`, and it should in fact apply to values of nullable *value* types as well as reference types. It "upgrades" the value to non-nullable, by throwing if it is null. + +``` c# +if (person.Kind == Student) +{ + List courses = person.Courses!; // I know it's not null for a student, but the compiler doesn't. + ... +} +``` + +The `!` operator naturally leads to `x!.y`, which is great! Although `!.` is two operators, it will feel as a cousin of `?.` (which is one operator). While the latter is conditional on null, the former just plows through. Naively, it implies two redundant null checks, one by `!` and one by `.`, but we'll optimize that of course. + +``` c# +if (person.Kind == Student) +{ + var passed = !person.Courses!.Any(c => c.Grade == F); + ... +} +``` + +Technically this would allow `x!?.y`, which comes quite close to swearing. We should consider warning when you use `?.` on non-null things. + +VB may have a problem with post-fix `!`. We'll cross that bridge when we get there. + + +# Generics and nullability + +Is it too heavyhanded to require `?` on constraints to allow nullable type arguments? + +Often, when you have a constraint it is because you want to operate on instances. So it's probably good that the default is not nullable. + +It may feel a bit egregious to require it on *all* the constraints of a type parameter, though. Should we put any `?`'s on the type parameter declaration instead of in the constraints? No, that is too weird and different. The case of multiple nullable constraints is probably sufficiently rare that it is reasonable to ask folks to put a `?` on each. In fact we should disallow having `?` on only some, since those question marks won't have an effect: they'll be cancelled by the non-nullable fellow constraints. + +The proposal talks about allowing `?` on the use of type parameters to explicitly override their nullness. Maybe we should have an explicit `!` as well, to explicitly override in the other direction: non-nullable. Think for instance of a `FirstNonNull` method. + +``` c# +T! FirstNonNull(IList list) { ... } +T? FirstOrDefault(IList list) { ... } +``` + +This means complexity slowly creeps into the proposal, thanks to generics. However, it seems those overrides are relatively rare, yet really useful when you need them. + +`T!` would only be allowed on type parameters, and only when they are not already non-null by constraint. + + diff --git a/meetings/LDM-2015-09-01.md b/meetings/LDM-2015-09-01.md new file mode 100644 index 0000000000..15ff5192db --- /dev/null +++ b/meetings/LDM-2015-09-01.md @@ -0,0 +1,156 @@ +C# Design Meeting Sep 1 2015 +============================ + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5233. + +Agenda +------ + +The meeting focused on design decisions for prototypes. There's no commitment for these decisions to stick through to a final feature; on the contrary the prototypes are being made in order to learn new things and improve the design. + +1. ref returns and ref locals +2. pattern matching + + +Ref returns and ref locals +========================== + +Proposal in #118, Initial PR for prototype in #4042. + +This feature is a generalization of ref parameters, and as such doesn't have much new, either conceptually or in how you work with it. As is already the case, refs cannot be null (they always reference a memory location), and you access the location they point to without explicitly dereferencing. The new thing is that you can return refs from methods and other functions, and that you can have locals that are refs to other memory locations. + +There's a new notion of "safe-to-return". It would be bad if you could return a ref to your own stack frame, for instance. Therefore there's a simple analysis to always track if a ref is "safe-to-return" - and a compile time error if you return something that isn't. + +A given method can get refs from a number of places: + +1. refs to locals in this method +2. ref parameters passed to it +3. refs into heap objects +4. refs returned from calls +5. ref locals in this method + +The 1st are never safe to return; the 2nd and 3rd always are. The 4th are safe to return if every ref passed *into* the call is safe to return - since we then know that the returned ref cannot be to a local of the calling (or called) method. + +The 5th is more involved, as it depends on which semantics we adopt for ref locals. + +Ref locals +---------- + +There are a couple of different questions to consider for ref locals: + +1. Can they be reassigned or are they fixed to a location at declaration time (like ref parameters)? +2. When are they safe to return? Always? Never? Depends? + +If ref locals can be reassignable *and* unsafe-to-return, that means that we have to worry about lifetime analysis: + +``` c# +ref int r; +while (...) +{ + int x = ...; + if (...) ref r = ref x; +} +WriteLine(r); // What now? +``` + +In other words we have to concern ourselves with the situation where locals are assigned to a ref that is longer lived than the local itself. We would have to detect it and either forbid it or resort to expensive techniques (similar to variable capture by lambdas) that run counter to the perf you were probably hoping for by using refs in the first place. + +On the other hand, if ref locals cannot be assigned after initialization, there will be no opportunity to assign variables to them that live in a more nested, and hence shorter-lived, scope. And if they are required to be safe-to-return, then *no* locals can be assigned to them, regardless of lifetime. Either of these seem more attractive! + +We think that not being able to assign locals to ref locals is too limiting. For instance, it would be common to keep a struct in a local, and then call a function with a ref to it to locate and return a ref to some data nested in it. You'd want to store the result of that in a ref local: + +``` c# +Node n = ...; struct +ref Node l = ref GetLeftMostSubNode(ref n); +l.Contents = ...; // modify leftmost node +``` + +So we definitely want to allow ref locals that are non-reassignable and unsafe-to-return. + +However, it is also reasonable to want to call similar helper methods on refs that *are* safe to return, to store the results and to then return them: + +``` c# +ref Node M(ref Node n) +{ + if (...) + { + ref Node l = ref GetLeftMostSubNode(ref n); + l.Contents = ...; // modify leftmost node + return ref l; + } + ... +} +``` + +It seems that we want ref locals to be safe-to-return or not *depending on what they are initialized with*. + +We think that there is also some value in having reassignable ref locals. For instance, you can imagine a ref local being the current "pointer" in a loop over a struct array. + +We can imagine a rule that simply says that ref locals are reassignable *except if they are initialized with an unsafe-to-return ref*. Then, only safe-to-return refs can be assigned to the reassignable ones, and we still don't have lifetime tracking issues. + +On closer scrutiny, though, this scenario does raise harder issues. If you use a ref as a current pointer, what's its initial value? Do we need null refs in the language now? How do you increment it? Can it point "past the edge" at the end? + +These are questions we don't want to answer right now for a somewhat hypothetical scenario. + +**Conclusion**: for the propotype, ref locals are non-reassignable, and get their "safe-to-return" status from their mandatory ref initializer. + +If there are scenarios not covered by this, we'll discover as folks start using the prototype and tell us. + +By avoiding reassignment we avoid answering a couple of difficult questions: +* What's the syntax of a ref assignment? `ref x = y`? `ref x = ref y`? `x = ref y`? +* Are ref assignments expressions, and what does that mean? +* Null/default values or definite assignment analysis to prevent the need for them + +`this` in structs +----------------- + +For non-readonly structs, `this` is generally described as being like a ref parameter. However, considering it safe-to-return like ref parameters in struct methods leads to undesirable behavior: It lets a ref to `this` be returned from the method. Therefore a caller needs to consider a ref returned from a method on a struct local *not* safe to return (the returned ref could be the struct local itself or part of it). In essence, the ref `this` "contaminates" any ref-returning method called on a local value type. + +Scenarios where this is a problem include using structs as wrapper types for heap objects. + +Somewhat counterintuitively perhaps, the better rule is to consider `this` in struct methods unsafe-to-return. Since struct methods will never return a ref to the receiver, you can call them with safe-to-return ref parameters and get safe-to-return ref results. + +This also solves a problem with generics, where you don't *know* if a receiver is going to be a struct. Now you don't have to program defensively against that; ref returning methods called on *anything* will be independent of their receiver with regards to safe-to-return determination. + +Other issues +------------ + +* We may want an unsafe language-level feature to convert between refs and pointers. We'll look at that later. +* PEVerify considers ref return unsafe. We need to chase that down, but for the prototype it's fine. +* It'd be useful to have a notion of "readonly refs" - refs through which you cannot write to the referenced storage. This is a separate (big) topic for later. + + +Pattern matching +================ + +Proposal in #206, initial PR in #4882. + +Syntax: There are patterns, extended is-expressions, and (later) switch. +``` c# +pattern: + * + type identifier // scope is a separate issue + type { id is pattern, ... } [identifier] + type (pattern, ...) + constant-expression + +is-expression: + relational-expression is pattern +``` + +For constants, we would have integral constants be flexible between types. Floats probably not. This has tons of levers, that we can obsess over, but in the prototype this is what we'll go with. + +Relational operators in patterns? `e is > 3`? Maybe later. + +For VB probably more issues. Save for later. + +Scope for variables: in scope in nearest enclosing statement, *except* the else-block of an if-statement. This is a weird but useful exception that allows subsequent else-if's to introduce variables with the same name as previous ones in the conditions. + +FOr now, the introduced variables are not reassignable. This could have both positive and negative perf impact. It's an opportunity to do "the right thing", but also a deviation from what we do with other variables. We'll try it in the prototype and see what it feels like. + +Deconstruction and guards are interesting further topics that we don't want to pursue yet. + +Record types are not part of this prototype. + +Some concerns about the active pattern part being too weird or different. Best resolved by prototyping it and playing with it. + diff --git a/meetings/LDM-2015-09-02.md b/meetings/LDM-2015-09-02.md new file mode 100644 index 0000000000..3341bdd72d --- /dev/null +++ b/meetings/LDM-2015-09-02.md @@ -0,0 +1,102 @@ +C# Design Notes Sep 2 2015 +========================== + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5234. + +Quote of the day: Are you pulling the curtain away from my magic retort? + +Agenda +------ + +1. Extending nameof +2. What do branches mean +3. Supersedes + + +Extending nameof +================ + +Should we allow empty type argument lists? When we first designed nameof the idea was to allow the same operands as typeof. During design we took it more in the direction of expressions, which seemed more useful. + +However, it leaves a nuisance of providing type arguments that don't really provide any value. + +One problem with this approach is what to do about situations like `MyList<>.First.P` where `First`'s type is the type parameter of `MyList`. + +We would think of `MyList<>` as having a type argument that has no members at all - not even the ones from object. Thus, if you dot on it, you get an error. + +More simply, just allow what's in typeof, followed by one member access. Make sure that, like in typeof, type arguments are either given everywhere or nowhere. + +For inherited members/nested types, we should do the same as typeof does. + +We don't expect this to upset analyzers much. We would make sure that the unbound types do have members in the semantic model. *Those* members just don't have members. Thus `MyList<>.First` is fine, but `MyList<>.First.P` is not. + +We like this. + +What do branches mean +===================== + +As prototypes start to come online, we need to decide how we use our pbranches. The Future branch is for when things are at prototype level for the whole experience - including IDE. Features will be guarded by a feature flag until it's the confident plan of record to ship the feature in its current form. + +We are working on enabling VSIXes to change the language model in the IDE without the need for customers to install over VS. Hopefully this capability will be available in an update to VS in a not too distant future. Once that is in place, we can ship prototypes that run without risk to customers' VS installation. + + +Supersedes +========== + +For metaprogramming, like Javas annotation processing tool (APT), we would like to facilitate generating additional code from existing code (e.g. based on attributes or patterns). Roslyn now provides an excellent stack for that. + +As a language C# is already better suited for this than Java because of partial types and methods: they allow the merging of existing and generated code to be less convoluted. + +However we can do even better. We can allow omitting `partial` on one of the parts, so the user written code doesn't need it. But more interestingly we can consider a `supersedes` feature (where the keyword `supersedes` may need to be replaced by something that people have less trouble spelling!): + +``` c# +class C +{ + public void M() + { + // do stuff + } + + [INPC] public int X { + get { ... } + set { ... } +} + +// generated +partial class C +{ + public supersedes void M() + { + // do something + superseded(); // calls original method + // do more + } + + public supersedes int X { + get { return superseded; } + set + { + superseded = value; + RaiseNPC(nameof(X)); + } + } +} +``` + +The compiler would generate code to keep the original `M` and `X` under a mangled name, inlined or something. + +Challenge: composing multiple rewriters: either we have an ordering rule, or we get errors. This is a challenging problem. + +Another good question: Do you have just one generator phase, or keep going to a fix point. Java keeps going until no changes. + +Is the generated code visible to the user? You should see it like generated code today, and the compiler would parse it again to guard from mistakes. That way, code could also be generated by t4 and other tools, as well as Roslyn. + +Concern: we are adding a new concept. Would it be better to use existing concepts, like Java does? + +Concern: efficiency. Forces binding, throws away first result and binds again against generated code. + +This is very interesting and promising, but there are many juicy challenges ahead. + + + + \ No newline at end of file diff --git a/meetings/LDM-2015-09-08.md b/meetings/LDM-2015-09-08.md new file mode 100644 index 0000000000..408c4dd2f6 --- /dev/null +++ b/meetings/LDM-2015-09-08.md @@ -0,0 +1,173 @@ +# C# Design Notes for Sep 8, 2015 + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5383. + +## Agenda + +1. Check-in on some of our desirable features to see if we have further thoughts +2. Start brainstorming on async streams + + +# Check-in on features + +## Bestest betterness + +Overload resolution will sometimes pick a candidate method only to lead to an error later on. For instance, it may pick an instance method where only a static one is allowed, a generic method where the constraints aren't satisfied or an overload the relies on a method group conversion that will ultimately fail. + +The reason for this behavior is usually to keep the rules simple, and to avoid accidentally picking another overload than what you meant. However, it also leads to quite a bit of frustration. It might be time to sharpen the rules. + +## Private protected + +In C# 6 we considered adding a new accessibility level with the meaning protected *and* internal (`protected internal` means protected *or* internal), but gave up because it's hard to settle on a syntax. We wanted to use `private protected` but got a lot of loud push back on it. + +No-one has come up with a better syntax in the meantime though. We are inclined to think that `private protected` just takes a little getting used to. We may want to try again. + +## Attributes + +There are a number of different features related to attributes. We should return to these in a dedicated meeting looking at the whole set of proposals together: + +* Generic attributes: They seem to be allowed by the runtime, but for some reason are disallowed by the language +* Compile-time only attributes: For attributes that are intended to be used only before IL gen, it'd be nice to have them elided and not bloat metadata +* Attributes in more places: There are more places where it makes sense to allow attributes, such as on lambda expressions. If they are compile-time only, they can even be in places where there is no natural code gen. +* Attributes of more types: For attributes that are not going into code gen, we could relax the restrictions on the types of their constructor parameters. + +A lot of these would be particularly helpful to Roslyn based tools such as analyzers and metaprogramming. + +## Local extern functions + +We should allow local functions to be extern. You'd almost always want to wrap an extern method in a safe to call wrapper method. + +## Withers for arbitrary types + +If we want to start focusing more on immutable data structures, it would be nice if there was a language supported way of creating new objects from existing ones, changing some subset of properties along the way: + +``` c# +var newIfStatement = ifStatement with { Condition = newCondition, Statement = newStatement }; // other properties copied +``` + +Currently the Roslyn code base, for example, uses the pattern of "withers", which are methods that return a new object with one property changed. There needs to be a wither per property, which is quite bloatful, and in Roslyn made feasible by having them automatically generated. Even so, changing multiple properties is suboptimal: + +``` c# +var newIfStatement = ifStatement.WithCondition(newCondition).WithStatement(newStatement); +``` + +It would be nice if we could come up with an efficient API pattern to support a built-in, efficient `with` expression. + +Related to this, it would also be nice to support object initializers on immutable objects. Again, we would need to come up with an API pattern to support it; possibly the same that would support the with expression. + +## Params IEnumerable + +This is a neat little feature that we ultimately rejected or didn't get to in C# 6. It lets you do away with the situation where you have to write two overloads of a method, one that takes an `IEnumerable` (for generality) and another one that takes `params T[]` and calls the first one with its array. + +The main problem raised against params IEnumerable is that it encourages an inefficient pattern for how parameters are captured: An array is allocated even when there are very few arguments (even zero!), and the implementation then accesses the elements through interface dispatches and further allocation (of an IEnumerator). + +Probably this won't matter for most people - they can start out this way, and then build a more optimal pattern if they need to. But it might be worthwhile considering a more general language pattern were folks can build a params implementation targeting something other than arrays. + + +# Async streams + +We shied back from a notion of asynchronous sequences when we originally introduced async to C#. Part of that was to see whether there was sufficient demand to introduce framework and language level concepts, and get more experience to base their design on. But also, we had some fear that using async on a per-element level would hide the true "chunky" degree of asynchrony under layers of fine-grained asynchronous abstractions, at great cost to performance. + +## IAsyncEnumerable + +At this point in time, though, we think that there is definitely demand for common abstractions and language support: foreach'ing, iterators, etc. Furthermore we think that the performance risks - allocation overhead in particular - of fine grained asynchrony can large be met with a combination of the compiler's existing optimizations and a straightforward asynchronous "translation" of `IEnumerable` into `IAsyncEnumerable`: + +``` c# +public interface IAsyncEnumerable +{ + public IAsyncEnumerator GetEnumerator(); +} + +public interface IAsyncEnumerator +{ + public T Current { get; } + public Task MoveNextAsync(); +} +``` + +The only meaningful difference is that the `MoveNext` method of the enumerator interface has been made async: it returns `Task` rather than `bool`, so that you need to await it to find out whether there is a next element (which you can then acquire from the `Current` property) or the sequence is finished. + +## Allocations + +Let's assume that you are foreach'ing over such an asynchronous sequence, which is buffered behind the scenes, so that 99.9% of the time an element is available locally and synchronously. Whenever a `Task` is awaited that is already completed, the compiler avoids the heavy machinery and just gets the value straight out of the task without pause. If *all* awaited Tasks in a given method call are already completed, then the method will never allocate a state machine, or a delegate to store as a continuation, since those are only constructed the first time they are needed. + +Even when the async method reaches its return statement synchronously, without the awaits having ever paused, it needs to construct a Task to return. So normally this would still require one allocation. However, the helper API that the compiler uses for this will actually cache completed Tasks for certain common values, including `true` and `false`. In summary, a `MoveNextAsync` call on a sequence that is buffered would typically not allocate anything, and the calling method often wouldn't either. + +The lesson is that fine-grained asynchrony is bad for performance if it is done in terms of `Task` where completed Tasks are never or rarely cached, e.g. `Task` or `Task`. It should be done in terms of `Task` or even non-generic `Task`. + +We think that there may or may not be scenarios where people want to get explicit about the "chunks" that data is transmitted in. If so, they can express this as `IAsyncEnumerable>` or some such thing. But there is no need to complicate asynchronous streaming by forcing people to deal with chunking by default. + +## Linq bloat + +Another concern is that there are many API's on `IEnumerable` today; not least the Linq query operators `Select`, `Where` and so on. Should all those be duplicated for `IAsyncEnumerable`? + +And when you think about it, we are not just talking about one extra set of overloads. Because once you have asynchronously foreach'able streams, you'll quickly want the delegates applied by the query operators to *also* be allowed to be async. So we have potentially four combinations: + +``` c# +public static IEnumerable Where(this IEnumerable source, Func predicate); +public static IAsyncEnumerable Where(this IAsyncEnumerable source, Func predicate); +public static IAsyncEnumerable Where(this IEnumerable source, Func> predicate); +public static IAsyncEnumerable Where(this IAsyncEnumerable source, Func> predicate); +``` + +So either we'd need to multiply the surface area of Linq by four, or we'd have to introduce some new implicit conversions to the language, e.g. from `IEnumerable` to `IAsyncEnumerable` and from `Func` to `Func>`. Something to think about, but we think it is probably worth it to get Linq over asynchronous sequences one way or another. + +Along with this, we'd need to consider whether to extend the query syntax in the language to also produce async lambdas when necessary. It may not be worth it - using the query operators may be good enough when you want to pass async lambdas. + +## Language support + +In the language we would add support for foreach'ing over async sequences to consume them, and for async iterators to produce them. Additionally (we don't discuss that further here) we may want to introduce a notion of `IAsyncDisposable`, for weach we could add an async version of the `using` statement. + +One concern about async versions of language features such as foreach (and using) is that they would generate `await` expressions that aren't there in source. Philosophically that may or may not be a problem: do you want to be able to see where all the awaiting happens in your async method? If that's important, we can maybe add the `await` or `async` keyword to these features somewhere: + +``` c# +foreach (string s in asyncStream) { ... } // or +foreach async (string s in asyncStream) { ... } // or +foreach (await string s in asyncStream) { ... } // etc. +``` + +Equally problematic is when doing things such as `ConfigureAwait`, which is important for performance reasons in libraries. If you don't have your hands on the Task, how can you `ConfigureAwait` it? The best answer is to add a `ConfigureAwait` extension method to `IAsyncEnumerable` as well. It returns a wrapper sequence that will return a wrapper enumerator whose `MoveNextAsync` will return the result of calling `ConfigureAwait` on the task that the wrapped enumerator's `MoveNextAsync` method returns: + +``` c# +foreach (string s in asyncStream.ConfigureAwait(false)) { ... } +``` + +For this to work, it is important that async foreach is pattern based, just like the synchronous foreach is today, where it will happily call any `GetEnumerator`, `MoveNext` and `Current` members, regardless of whether objects implement the official "interfaces". The reason for this is that the result of `Task.ConfigureAwait` is not a `Task`. + +A related issue is cancellation, and whether there should be a way to flow a `CancellationToken` to the `MoveNextAsync` method. It probably has a similar solution to `ConfigureAwait`. + +## Channels in Go + +The Go language has a notion of channels, which are communication pipes between threads. They can be buffered, and you can put things in and take them out. If you put things in while the channel is full, you wait. If you take things out while it is empty, you wait. + +If you imagine a `Channel` abstraction in .NET, it would not have blocking waits on the endpoints; those would instead be asynchronous methods returning Tasks. + +Go has an all-powerful language construct called `select` to consume the first available element from any set of channels, and choosing the logic to apply to that element based on which channel it came from. It is guaranteed to consume a value from only one of the channels. + +It is worthwhile for us to look at Go channels, learn from them and consider to what extent a) we need a similar abstraction and b) it is connected to the notion of async streams. + +Some preliminary thoughts: Channels and select statements are very easy to understand, conceptually. On the other hand they are somewhat low-level, and extremely imperative: there is a strong coupling from the consumer to the producer, and in practice there would typically only *be* one consumer. It seems like a synchronization construct like semaphores or some of the types from the DataFlow library. + +The "select" functionality is interesting to ponder more generally. If you think about it from an async streams perspective, maybe the similar thing you would do would be to merge streams. That would need to be coupled with the ability to tie different functionality to elements from different original streams - or with different types. Maybe pattern matching is our friend here? + +``` c# +foreach (var e in myInts.Merge(myStrings)) +{ + switch (e) + { + case int i: + ... + case string s: + ... + } +} +``` + +Either way, it isn't as elegant by a long shot. If we find it's important, we'd need to consider language support. + +Another important difference between IAsyncEnumerable and Channels is that an enumerable can have more than one consumer. Each enumerator is independent of the others, and provides access to all the members - at least from the point in time where it is requested. + + +## Conclusion + +We want to keep thinking about async streams, and probably do some prototyping. \ No newline at end of file diff --git a/meetings/LDM-2015-10-07 (Design Review Notes).md b/meetings/LDM-2015-10-07 (Design Review Notes).md new file mode 100644 index 0000000000..02447f947d --- /dev/null +++ b/meetings/LDM-2015-10-07 (Design Review Notes).md @@ -0,0 +1,135 @@ +Notes on Records and Pattern Matching for 2015-10-07 design review +================================================================== + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/5757. + +### Records and Pattern Matching (https://github.com/dotnet/roslyn/issues/206) + +Prototyped together last year by @semihokur, but that prototype is +based on very out-of-date Roslyn code. We also have some design changes +since that time and we want to separate a pattern-matching prototype from +records/ADTs so we can make independent decisions about whether +and when to include them in the language(s). + +First step is to port pattern matching to the latest sources. +In-progress port at https://github.com/dotnet/roslyn/pull/4882 + +### Spec changes since the 2014 prototype + +For pattern matching: + +1. Scoping of pattern-introduced variables (with "funny" rule for `if`) +2. Rules for `switch` statement that make it a compatible extension of the existing construct (https://github.com/dotnet/roslyn/issues/4944) +3. An expression form of multi-arm pattern-matching (https://github.com/dotnet/roslyn/issues/5154) +4. A `when` clause added to `switch` cases. + +And, for records: + +1. No `record` keyword necessary +2. `with` expressions (https://github.com/dotnet/roslyn/issues/5172) +3. Approach for for algebraic data types + +### Implementation status of prototype port + +1. For pattern matching, checklist at https://github.com/dotnet/roslyn/pull/4882 tracking the progress +2. For records, port not started + +### Making the extension of `switch` backward-compatible + +- We say that the cases are matched in order, except `default` which is always the last +resort. + +- Integral-typed case labels match any integral-valued control expression with the same value. + +- One issue around user-defined conversions to switchable types is +resolved (https://github.com/dotnet/roslyn/issues/4944). In the draft spec, +a conversion will be applied on the `case`s, not on the control-expression unilaterally. +Instead of converting only to `swithable` types, each +`case` arm will consider any conversions that allow the `case` to be applied. +Any given conversion would be applied at most once. + +```cs +Foo foo = ...; // has a conversion to int +switch (foo) +{ + case 1: // uses the converted value + case Foo(2): // uses the original value + case 3: // uses the converted value +} +``` + +- The `goto case` statement is extended to allow any expression as its argument. + +### Expression form of multi-arm pattern matching (https://github.com/dotnet/roslyn/issues/5154) + +```cs +var areas = + from primitive in primitives + let area = primitive match ( + case Line l: 0 + case Rectangle r: r.Width * r.Height + case Circle c: Math.PI * c.Radius * c.Radius + case *: throw new ApplicationException() + ) + select new { Primitive = primitive, Area = area }; +``` + +There is no `default` here, so cases are handled strictly in order. + +I propose the spec require that the compiler "prove" that all cases are handled +in a `match` expression using not-yet-specified rules. Writing those rules +is an open work item, but I imagine it will require the compiler to build +a decision tree and check it for completeness. That will also be needed to +implement checks that no case is subsumed by a previous case, which will +cause a warning (for `switch`) or error (for `match`). + +### With-expressions (https://github.com/dotnet/roslyn/issues/5172) + +```cs +class Point(int X, int Y, int Z); +... + Point p = ...; + Point q = p with { Y = 2 }; +``` + +The latter is translated into + +```cs + Point q = new Point(X: p.X, Y: 2, Z: p.Z); +``` + +We know how to do this for record types (because the language specifies the +mapping between constructor parameters and properties). We're examining how +to extend it to more general types. + +To support inheritance, rather than directly using the constructor (as above) the generated code will +invoke a compiler-generated (but user-overridable) factory method. + +```cs + Point q = p.With(X: p.X, Y: 2, Z: p.Z); +``` + +### Draft approach for algebraic data types + +```cs +abstract sealed class Expression +{ + class Binary(Operator Operator, Expression Left, Expression Right) : Expression; + class Constant(int Value) : Expression; +} +``` + +None of these classes would be permitted to be extended elsewhere. +a `match` expression that handles both `Binary` and `Constant` cases +would not need a `*` (default) case, as the compiler can prove it +is complete. + +### Remaining major issues + +1. We need to specify the rules for checking + - If the set of cases in a `match` is complete + - If a `case` is subsumed by a previous `case` + +2. We need more experience with algebraic data types and active patterns. + +3. Can we extend `with` expressions to non-record types? diff --git a/meetings/LDM-2015-11-02 (Design Demo).md b/meetings/LDM-2015-11-02 (Design Demo).md new file mode 100644 index 0000000000..392e19ce6b --- /dev/null +++ b/meetings/LDM-2015-11-02 (Design Demo).md @@ -0,0 +1,251 @@ +Here’s an outline of a demo at the MVP summit on 2015-11-02 +=========================================================== + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/6505. + +### Let’s talk about local functions. + +A method often has other private “helper” methods that are used in its implementation. Those methods are in the scope of the enclosing type, even though they are only intended to be used in a single place. Local functions allow you to define a function where it is used. For example, given a helper method + + static int Fib(int n) + { + return (n < 2) ? 1 : Fib(n - 1) + Fib(n - 2); + } + +Or, using the new syntax added in C# 6: + + static int Fib(int n) => (n < 2) ? 1 : Fib(n - 1) + Fib(n - 2); + +And the method that it is used in + + static void Main(string[] args) + { + Console.WriteLine(Fib(7)); + Console.ReadKey(); + } + +In C# 7 you’ll be able to define the helper function in the scope where it is used: + + static void Main(string[] args) + { + int Fib(int n) => (n < 2) ? 1 : Fib(n - 1) + Fib(n - 2); //! + + Console.WriteLine(Fib(7)); + Console.ReadKey(); + } + +Local functions can use variables from the enclosing scope: + + static void Main(string[] args) + { + int fib0 = 1; //! + int Fib(int n) => (n < 2) ? fib0 : Fib(n - 1) + Fib(n - 2); + + Console.WriteLine(Fib(7)); + Console.ReadKey(); + } + +You can imagine having to pass such state as additional parameters to a helper method if it were declared in the enclosing type, but local function can use local variables directly. + +Capturing state like this does not require allocating frame objects on the heap as it would for delegates, or allocating a delegate object either, so this is much more efficient than what you would have to do to simulate this feature by hand. + +### Let’s talk about pattern matching. + +With object-oriented programming, you define a virtual method when you have to dispatch an operation on the particular kind of object. That works best when the author of the types can identify ahead of time all of the operations (virtual methods) on the types, but it enables you to have an open-ended set of types. + +In the functional style, on the other hand, you define your data as a set of types without virtual functions, and define the functions separately from the data. Each operation provides an implementation for each type in the type hierarchy. That works best when the author of the types can identify ahead of time all of the shapes of the data, but it enables you to have an open-ended set of operations. + +C# does a great job for the object-oriented style, but the functional style (where you cannot identify all the operations ahead of time) shows up as a frequent source of awkwardness in C# programs. + +Let’s get really concrete. Suppose I have a small hierarchy of types + + // class Person(string Name); + class Person + { + public Person(string name) { this.Name = name; } + public string Name { get; } + } + + // class Student(string Name, double Gpa) : Person(Name); + class Student : Person + { + public Student(string name, double gpa) : base(name) + { this.Gpa = gpa; } + public double Gpa { get; } + } + + // class Teacher(string Name, string Subject) : Person(Name); + class Teacher : Person + { + public Teacher(string name, string subject) : base(name) + { this.Subject = subject; } + public string Subject { get; } + } + +The comments, by the way, shows a possible future syntax we are considering for C# 7 that we call records. We’re still working on records, so I won’t say more about that today. Here is an operation that uses these types + + static string PrintedForm(Person p) + { + Student s; + Teacher t; + if ((s = p as Student) != null && s.Gpa > 3.5) + { + return $"Honor Student {s.Name} ({s.Gpa})"; + } + else if (s != null) + { + return $"Student {s.Name} ({s.Gpa})"; + } + else if ((t = p as Teacher) != null) + { + return $"Teacher {t.Name} of {t.Subject}"; + } + else + { + return $"Person {p.Name}"; + } + } + +And for the purposes of the demo, a client of that operation + + static void Main(string[] args) + { + Person[] oa = { + new Student("Einstein", 4.0), + new Student("Elvis", 3.0), + new Student("Poindexter", 3.2), + new Teacher("Feynmann", "Physics"), + new Person("Anders"), + }; + foreach (var o in oa) + { + Console.WriteLine(PrintedForm(o)); + } + Console.ReadKey(); + } + +Note the need to declare the variables `s` and `t` ahead of time in `PrintedForm`. Even though they are only used in one branch of the series of if-then-else statements, they are in scope throughout. That means that you have to think up distinct names for all of these temporary variables. As part of the pattern-matching feature we are repurposing the “is” operator to take a pattern on the right-hand-side. And one kind of pattern is a variable declaration. That allows us to simplify the code like this + + static string PrintedForm(Person p) + { + if (p is Student s && s.Gpa > 3.5) //! + { + return $"Honor Student {s.Name} ({s.Gpa})"; + } + else if (p is Student s) + { + return $"Student {s.Name} ({s.Gpa})"; + } + else if (p is Teacher t) + { + return $"Teacher {t.Name} of {t.Subject}"; + } + else + { + return $"Person {p.Name}"; + } + } + + +Now the temporary variables `s` and `t` are declared and scoped to just the place they need to be. Unfortunately we’re testing against the type `Student` more than once. Back to that in a moment. + +We’ve also repurposed the `switch` statement so that the case branches are patterns instead of just constants (though constants are one kind of pattern). That enables you to use `switch` as a "type switch": + + static string PrintedForm(Person p) + { + switch (p) //! + { + case Student s when s.Gpa > 3.5 : + return $"Honor Student {s.Name} ({s.Gpa})"; + case Student s : + return $"Student {s.Name} ({s.Gpa})"; + case Teacher t : + return $"Teacher {t.Name} of {t.Subject}"; + default : + return $"Person {p.Name}"; + } + } + +The compiler is careful so that we don’t type-test against `Student` more than once in the generated code for `switch`. + +Note the new `when` clause in the switch statement. + +We’re also working on an expression equivalent to the switch statement, which is like a multi-branch `?:` operator for pattern matching: + + static string PrintedForm(Person p) + { + return p match ( //! + case Student s when s.Gpa > 3.5 : + $"Honor Student {s.Name} ({s.Gpa})" + case Student s : + $"Student {s.Name} ({s.Gpa})" + case Teacher t : + $"Teacher {t.Name} of {t.Subject}" + case * : + $"Person {p.Name}" + ); + } + +Because you sometimes need to throw an exception when some condition is unexpected, we’re adding a *throw expression* that you can use in a match expression: + + return p match ( + case Student s when s.Gpa > 3.5 : + $"Honor Student {s.Name} ({s.Gpa})" + case Student s : + $"Student {s.Name} ({s.Gpa})" + case Teacher t : + $"Teacher {t.Name} of {t.Subject}" + case null : + throw new ArgumentNullException(nameof(p)) //! + case * : + $"Person {p.Name}" + ); + +Another useful kind of pattern allows you to match on members of a type: + + return p match ( + case Student s when s.Gpa > 3.5 : + $"Honor Student {s.Name} ({s.Gpa})" + case Student { Name is "Poindexter" } : //! + "A Nerd" + case Student s : + $"Student {s.Name} ({s.Gpa})" + case Teacher t : + $"Teacher {t.Name} of {t.Subject}" + case null : + throw new ArgumentNullException(nameof(p)) + case * : + $"Person {p.Name}" + ); + +Since this is an expression, we can use the new “=>” form of a method. Our final method is + + static string PrintedForm(Person p) => p match ( + case Student s when s.Gpa > 3.5 : + $"Honor Student {s.Name} ({s.Gpa})" + case Student { Name is "Poindexter" } : + "A Nerd" + case Student s : + $"Student {s.Name} ({s.Gpa})" + case Teacher t : + $"Teacher {t.Name} of {t.Subject}" + case null : + throw new ArgumentNullException(nameof(p)) + case * : + $"Person {p.Name}" + ); + +In summary: +- Local functions (capturing state is cheap) +- Pattern-matching + - Operator is + - Switch, “when” clauses + - `match` expression + - Patterns + - Constant patterns e.g. `1` in an ordinary switch + - type-match patterns e.g. `Student s` + - property patterns e.g. `Student { Name is "Poindexter" }` + - wildcard e.g. `*` +- Still working on + - Records, algebraic data types + - Tuples diff --git a/meetings/LDM-2016-02-29.md b/meetings/LDM-2016-02-29.md new file mode 100644 index 0000000000..d7bc7aa198 --- /dev/null +++ b/meetings/LDM-2016-02-29.md @@ -0,0 +1,188 @@ +C# Language Design Notes Feb 29, 2016 +===================================== + +Discussion for these notes can be found at https://github.com/dotnet/roslyn/issues/9330. + +## Catch up edition (deconstruction and immutable object creation) + +Over the past couple of months various design activities took place that weren't documented in design notes. The following is a summary of the state of design regarding positional deconstruction, with-expressions and object initializers for immutable types. + +# Philosophy + +We agree on the following design tenets: + +Positional deconstruction, with-expressions and object initializers are separable features, enabled by the presence of certain API patterns on types that can be expressed manually, as well as generated by other language features such as records. + +# API Patterns + +API patterns for a language feature facilitate two things: + +* Provide actual APIs to call _at runtime_ when the language feature is used +* Inform the compiler _at compile time_ about how to generate code for the feature + +It turns out the biggest design challenges are around the second part. Specifically, all these API patterns turn out to need to bridge between positional and name-based expressions of the members of types. How each API pattern does that is a central question of its design. + +Assume the following running example: + +``` c# +public class Person +{ + public string FirstName { get; } + public string LastName { get; } + + public Person(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } +} +``` + +In the following we'll consider extending and changing this type to expose various API patterns as we examine the individual language features. + +Here's an example of using the three language features: + +``` c# +var p = new Person { FirstName = "Mickey", LastName = "Mouse" }; // object initializer +if (p is Person("Mickey", *)) // positional deconstruction +{ + return p with { FirstName = "Minney" }; // with-expression +} +``` + +Semantically this corresponds to something like this: + +``` c# +var p = new Person("Mickey", "Mouse"); // constructor call +if (p.FirstName == "Mickey") // property access +{ + return new Person("Minney", p.LastName); // constructor call +} +``` + +Notice how the new features that use property names correspond to API calls using positional parameters, whereas the feature that uses positions corresponds to member access by name! + +# Object initializers for immutable objects +(See e.g. #229) + +This feature allows an object initializer for which assignable properties are not found, to fall back to a constructor call taking the properties' new values as arguments. + +``` c# +new Person { FirstName = "Mickey", LastName = "Mouse" } +``` +becomes +``` c# +new Person("Mickey", "Mouse") +``` + +The question then is: how does the compiler decide to pass the given FirstName as the first argument? Somehow it needs clues from the `Person` type as to which properties correspond to which constructor parameters. These clues cannot just be the constructor body: we need this to work across assemblies, so the clues must be evident from metadata. + +Here are some options: + +1: The type or constructor explicitly includes metadata for this purpose, e.g. in the form of attributes. +2: The names of the constructor parameters must match exactly the names of the corresponding properties. + +The former is unattractive because it requires the type's author to write those attributes. It requires the type to be explicitly edited for the purpose. + +The latter is better in that it doesn't require extra API elements. However, API design guidelines stipulate that public properties start with uppercase, and parameters start with lower case. This pattern would break that, and for the same reason is highly unlikely to apply to any existing types. + +This leads us to: + +3: The names of the constructor parameters must match the names of the corresponding properties, _modulo case_! + +This would allow a large number of existing types to just work (including the example above), but at the cost of introducing case insensitivity to this part of the C# language. + +# With-expressions +(see e.g. #5172) + +With-expressions are similar to object initializers, except that they provide a source object from which to copy all the properties that aren't specified. Thus it seems reasonable to use a similar strategy for compilation; to call a constructor, this time filling in missing properties by accessing those on the source object. + +Thus the same strategies as above would apply to establish the connection between properties and constructor parameters. + +``` c# +p with { FirstName = "Minney" } +``` +becomes +``` c# +new Person("Minney", p.LastName) +``` + +However, there's a hitch: if the runtime source object is actually of a derived type with more properties than are known from its static type, it would typically be expected that those are copied over too. In that case, the static type is also likely to be abstract (most base types are), so it wouldn't offer a callable constructor. + +For this situation there needs to be a way that an abstract base class can offer "with-ability" that correctly copies over members of derived types. The best way we can think of is to offer a virtual `With` method, as follows: + +``` c# +public abstract class Person +{ + ... + public abstract Person With(string firstName, string lastName); +} +``` + +In the presence of such a `With` method we would generate a with expression to call that instead of the constructor: + +``` c# +p.With("Minney", p.LastName) +``` + +We can decide whether to make with-expressions _require_ a `With` method, or fall back to constructor calls in its absence. + +If we _require_ a `With` method, that makes for less interoperability with existing types. However, it gives us new opportunities for how to provide the position/name mapping metadata thorugh the declaration of that `With` method: For instance, we could introduce a new kind of default parameter that explicitly wires the parameter to a property: + +``` c# + public abstract Person With(string firstName = this.FirstName, string lastName = this.LastName); +``` + +To explicitly facilitate interop with an existing type, a mandatory `With` method could be allowed to be provided as an extension method. It is unclear how that would work with the default parameter approach, though. + +# Positional deconstruction +(see e.g. #206) + +This feature allows a positional syntax for extracting the property values from an object, for instance in the context of pattern matching, but potentially also elsewhere. + +Ideally, a positional deconstruction would simply generate an access of each member whose value is obtained: + +``` c# +p is Person("Mickey", *) +``` +becomes +``` c# +p.FirstName == "Mickey" +``` + +Again, this requires the compiler's understanding of how positions correspond to property names. Again, the same strategies as for object initializers are possible. See e.g. #8415. + +Additionally, just as in with-expressions, one might wish to override the default behavior, or provide it if names don't match. Again, an explicit method could be used: + +``` c# +public abstract class Person +{ + ... + public void Person GetValues(out string firstName, out string lastName); +} +``` +There are several options as to the shape of such a method. Instead of out-parameters, it might return a tuple. This has pros and cons: there could be only one tuple-returning `GetValues` method, because there would be no parameters to distinguish signatures. This may be a good or a bad thing. + +Just as the `With` method, we can decide whether deconstruction should _require_ a `GetValues` method, or should fall back to metadata or to name matching against the constructor's parameter names. + +If the `GetValues` method is used, the compiler doesn't need to resolve between positions and properties: the deconstruction as well as the method are already positional. We'd generate the code as follows: + +``` c# +string __p1; +string __p2; +p.GetValues(out __p1, out __p2); +... +__p1 == "Mickey" +``` + +Somewhat less elegant for sure, and possibly less efficient, since the `LastName` is obtained for no reason. However, this is compiler generated code that no one has to look at, and it can probably be optimized, so this may not be a big issue. + +# Summary + +For each of these three features we are grappling with the position-to-property match. Our options: + +1. Require specific metadata +2. Match property and parameter names, possibly in a case sensitive way +3. For deconstruction and with-expressions, allow or require specific methods (`GetValues` and `With` respectively) to implement their behavior, and possibly have special syntax in `With` methods to provide the name-to-position matching. + +We continue to work on this. \ No newline at end of file diff --git a/meetings/LDM-2016-04-06.md b/meetings/LDM-2016-04-06.md new file mode 100644 index 0000000000..73c9de2e64 --- /dev/null +++ b/meetings/LDM-2016-04-06.md @@ -0,0 +1,267 @@ +C# Design Notes for Apr 6, 2016 +=============================== + +Discussion for these design notes can be found at https://github.com/dotnet/roslyn/issues/10429. + +We settled several open design questions concerning tuples and pattern matching. + +# Tuples + +## Identity conversion + +Element names are immaterial to tuple conversions. Tuples with the same types in the same order are identity convertible to each other, regardless of the names. + +That said, if you have an element name at *one* position on one side of a conversion, and the same name at *another* position on the other side, you almost certainly have bug in your code: + +``` c# +(string first, string last) GetNames() { ... } +(string last, string first) names = GetNames(); // Oops! +``` + +To catch this glaring case, we'll have a warning. In the unlikely case that you meant to do this, you can easily silence it e.g. by assigning through a tuple without names at all. + +## Boxing conversion + +As structs, tuples naturally have a boxing conversion. Importantly, the names aren't part of the runtime representation of tuples, but are tracked only by the compiler. Thus, once you've "cast away" the names, you cannot recover them. In alignment with the identity conversions, a boxed tuple will unbox to any tuple type that has the same element types in the same order. + +## Target typing + +A tuple literal is "target typed" whenever possible. What that means is that the tuple literal has a "conversion from expression" to any tuple type, as long as the element expressions of the tuple literal have an implicit conversion to the element types of the tuple type. + +``` c# +(string name, byte age) t = (null, 5); // Ok: the expressions null and 5 convert to string and byte +``` + +In cases where the tuple literal is not part of a conversion, it acquires its "natural type", which means a tuple type where the element types are the types of the constituent expressions. Since not all expressions have types, not all tuple literals have a natural type either: + +``` c# +var t = ("John", 5); // Ok: the type of t is (string, int) +var t = (null, 5); // Error: null doesn't have a type +``` + +A tuple literal may include names, in which case they become part of the natural type: + +``` c# +var t = (name: "John", age: 5); // The type of t is (string name, int age) +``` + +## Conversion propagation + +A harder question is whether tuple types should be convertible to each other based on conversions between their element types. Intuitively it seems that implicit and explicit conversions should just "bleed through" and compose to the tuples. This leads to a lot of complexity and hard questions, though. What kind of conversion is the tuple conversion? Different places in the language place different restrictions on which conversions can apply - those would have to be "pushed down" as well. + +``` c# +var t1 = ("John", 5); // (string, int) +(object, long) t2 = t1; // What kind of conversion is this? Where is it allowed +``` + +On the whole we think that, while intuitive, the need for such conversions isn't actually that common. It's hard to construct an example that isn't contrived, involving for instance tuple-typed method parameters and the like. When you really need it, you can deconstruct the tuple and reconstruct it with a tuple literal, making use of target typing. + +We'll keep an eye on it, but for now the decision is *not* to propagate element conversions through tuple types. We do recognize that this is a decision we don't get to change our minds on once we've shipped: adding conversions in a later version would be a significant breaking change. + +## Projection initializers + +Tuple literals are a bit like anonymous types. The latter have "projection initializers" where if you don't specify a member name, one will be extracted from the given expression, if possible. Should we do that for tuples too? + +``` c# +var a = new { Name = c.FirstName, c.Age }; // Will have members Name and Age +var t = (Name: c.FirstName, c.Age); // (string Name, int Age) or error? +``` + +We don't think so. The difference is that names are optional in tuples. It'd be too easy to pick up a random name by mistake, or get errors because two elements happen to pick up the same name. + +## Extension methods on tuples + +This should just work according to existing rules. That means that extension methods on a tuple type apply even to tuples with different element names: + +``` c# +static void M(this (int x, int y) t) { ... } + +(int a, int b) t = ...; +t.M(); // Sure +``` + +## Default parameters + +Like other types, you can use `default(T)` to specify a default parameter of tuple type. Should you also be allowed to specify a tuple literal with suitably constant elements? + +``` c# +void M((string, int) t = ("Bob", 7)) { ... } // Allowed? +``` + +No. We'd need to introduce a new attribute for this, and we don't even know if it's a useful scenario. + +## Syntax for 0-tuples and 1-tuples? + +We lovingly refer to 0-tuples as nuples, and 1-tuples as womples. There is already an underlying `ValueTuple` of size one. We should will also have the non-generic `ValueTuple` be an empty struct rather than a static class. + +The question is whether nuples and womples should have syntactic representation as tuple types and literals? `()` would be a natural syntax for nuples (and would no doubt find popularity as a "unit type" alternative to `void`), but womples are harder: parenthesized expressions already have a meaning! + +We made no final decisions on this, but won't pursue it for now. + +## Return tuple members directly in scope + +There is an idea to let the members of a tuple type appearing in a return position of a method be in scope throughout the method: + +``` c# +(string first, string last) GetName() +{ + first = ...; last = ...; // Assign the result directly + return; // No need to return an explicit value +} +``` + +The idea here is to enhance the symmetry between tuple types and parameter lists: parameter names are in scope, why should "result names"? + +This is cute, but we won't do it. It is too much special casing for a specific placement of tuple types, and it is also actually preferable to be able to see exactly what is returned at a given `return` statement. + + +# Integrating pattern matching with is-expressions and switch-statements +For pattern matching to feel natural in C# it is vital that it is deeply integrated with existing related features, and does in fact take its queues from how they already work. Specifically we want to extend is-expressions to allow patterns where today they have types, and we want to augment switch-statements so that they can switch on any type, use patterns in case-clauses and add additional conditions to case-clauses using when-clauses. + +This integration is not always straightforward, as witnessed by the following issues. In each we need to decide what patterns should *generally* do, and mitigate any breaking changes this would cause in currently valid code. + + +## Name lookup + +The following code is legal today: + +``` c# +if (e is X) {...} +switch(e) { case X: ... } +``` + +We'd like to extend both the places where `X` occurs to be patterns. However, `X` means different things in those two places. In the `is` expression it must refer to a type, whereas in the `case` clause it must refer to a constant. In the `is` expression we look it up as a type, ignoring any intervening members called `X`, whereas in the `case` clause we look it up as an expression (which can include a type), and give an error if the nearest one found is not a constant. + +As a pattern we think `X` should be able to both refer to a type and a constant. Thus, we prefer the `case` behavior, and would just stop giving an error when `case X:` refers to a type. For `is` expressions, to avoid a breaking change, we will first look for just a type (today's behavior), and if we don't find one, rather than error we will look again for a constant. + +## Conversions in patterns + +An `is` expression today will only acknowledge identity, reference and boxing conversions from the run-time value to the type. It looks for "the actual" type, if you will, without representation changes: + +``` c# +byte b = 5; +WriteLine(b is byte); // True: identity conversion +WriteLine((object)b is byte); // True: boxing conversion +WriteLine((object)b is object); // True: reference conversion +WriteLine(b is int); // False: numeric conversion changes representation +``` + +This seems like the right semantics for "type testing", and we want those to carry over to pattern matching. + +Switch statements are more weird here today. They have a fixed set of allowed types to switch over (primitive types, their nullable equivalents, strings). If the expression given has a different type, but has a unique implicit conversion to one of the allowed ones, then that conversion is applied! This occurs mainly (only?) when there is a user defined conversion from that type to the allowed one. + +That of course is intended only for constant cases. It is not consistent with the behavior we want for type matching per the above, and it is also not clear how to generalize it to switch expressions of arbitrary type. It is behavior that we want to limit as much as possible. + +Our solution is that in switches *only* we will apply such a conversion on the incoming value *only* if all the cases are constant. This means that if you add a non-constant case to such a switch (e.g. a type pattern), you will break it. We considered more lenient models involving applying non-constant patterns to the *non*-converted input, but that just leads to weirdness, and we don't think it's necessary. If you *really* want your conversion applied, you can always explicitly apply it to the switch expression yourself. + + +## Pattern variables and multiple case labels + +C# allows multiple case labels on the same body. If patterns in those case labels introduce variables, what does that mean? + +``` c# +case int i: +case byte b: + WriteLine("Integral value!"); + break; +``` + +Here's what it means: The variables go into the same declaration space, so it is an error to introduce two of the same name in case clauses for the same body. Furthermore, the variables introduced are not definitely assigned, because the given case clause assigns them, and you didn't necessarily come in that way. So the above example is legal, but the body cannot read from the variables `i` and `b` because they are not definitely assigned. + +It is tempting to consider allowing case clauses to share variables, so that they could be extracted from similar but different patterns: +``` c# +case (int age, string name): +case (string name, int age): + WriteLine($"{name} is {age} years old."); + break; +``` + +We think that is way overboard right now, but the rules above preserve our ability to allow it in the future. + +## Goto case + +It is tempting to ponder generalizations of `goto case x`. For instance, maybe you could do the whole switch again, but on the value `x`. That's interesting, but comes with lots of complications and hidden performance traps. Also it is probably not all that useful. + +Instead we just need to preserve the simple meaning of `goto case x` from current switches: it's allowed if `x` is constant, if there's a case with the same constant, and that case doesn't have a `when` clause. + +## Errors and warnings + +Today `3 is string` yields a warning, while `3 as string` yields and error. They philosophy seems to be that the former is just asking a question, whereas the other is requesting a value. Generalized `is` expressions like `3 is string s` are sort of a combination of `is` and `as`, both answering the question and (conditionally) producing a value. Should they yield warnings or errors? + +We didn't reach consensus and decided to table this for later. + +## Constant pattern equality + +In today's `switch` statement, the constants in labels must be implicitly convertible to the governing type (of the switch expression). The equality is then straightforward - it works the same as the `==` operator. This means that the following case will print `Match!`. + +``` c# +switch(7) +{ + case (byte)7: + WriteLine("Match!"); + break; +} +``` + +What should be the case if we switch on something of type object instead?: + +``` c# +switch((object)7) +{ + case (byte)7: + WriteLine("Match!"); + break; +} +``` + +One philosophy says that it should work the same way regardless of the static type of the expression. But do we want constant patterns everywhere to do "intelligent matching" of integral types with each other? That certainly leads to more complex runtime behavior, and would probably require calling helper methods. And what of other related types, such as `float` and `double`? There isn't similar intelligent behavior you can do, because the representations of most numbers will differ slightly and a number such as 2.1 would thus not be "equal to itself" across types anyway. + +The other option is to make the behavior different depending on the compile-time type of the expression. We'll use integral equality only if we know statically which one to pick, because the left hand side was also known to be integral. That would preserve the switch behavior, but make the pattern's behavior dependent on the static type of the expression. + +For now we prefer the latter, as it is simpler. + + +# Recursive patterns + +There are several core design questions around the various kinds of recursive patterns we are envisioning. However, they seem to fall in roughly two categories: + +1. Determine the syntactic shape of each recursive pattern in itself, and use that to ensure that the places where patterns can occur are syntactically well-formed and unambiguous. +2. Decide exactly how the patterns work, and what underlying mechanisms enable them. + +This is an area to focus more on in the future. For now we're just starting to dig in. + +## Recursive pattern syntax + +For now we envision three shapes of recursive patterns + +1. Property patterns: `Type { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }` +2. Tuple patterns: `(Pattern_1, ... Pattern_n)` +3. Positional patterns: `Type (Pattern_1, ... Pattern_n)` + +There's certainly room for evolution here. For instance, it is not lost on us that 2 and 3 are identical except for the presence of a type in the latter. At the same time, the presence of a type in the latter seems syntactically superfluous in the cases where the matched expression is already known to be of that type (so the pattern is used purely for deconstruction and/or recursive matching of the elements). Those two observations come together to suggest a more orthogonal model, where the types are optional: + +1. Property patterns: `Type_opt { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }` +2. Positional patterns: `Type_opt (Pattern_1, ... Pattern_n)` + +In this model, what was called "tuple patterns" above would actually not just apply to tuples, but to anything whose static type (somehow) specifies a suitable deconstruction. + +This is important because it means that "irrefutable" patterns - ones that are known to always match - never need to specify the type. This in turn means that they can be used for unconditional deconstruction even in syntactic contexts where positional patterns would be ambiguous with invocation syntax. For instance, we could have what would amount to a "deconstruction" variant of a declaration statement, that would introduce all its match variables into scope as local variables for subsequent statements: + +``` c# +(string name, int age) = GetPerson(); // Unconditional deconstruction +WriteLine($"{name} is {age} years old"); // name and age are in scope + +``` + +## How recursive patterns work + +Property patterns are pretty straightforward - they translate into access of fields and properties. + +Tuple patterns are also straightforward if we decide to handle them specially. + +Positional patterns are more complex. We agree that they need a way to be specified, but the scope and mechanism for this is still up for debate. For instance, the `Type` in the positional pattern may not necessarily trigger a type test on the object. Instead it may name a class where a more general "matcher" is defined, which does its own tests on the object. This could be complex stuff, like picking a string apart to see if it matches a certain format, and extracting certain information from it if so. + +The syntax for declaring such "matchers" may be methods, or a new kind of user defined operator (like `is`) or something else entirely. We still do not have consensus on either the scope or shape of this, so there's some work ahead of us. + +The good news is that we can add pattern matching in several phases. There can be a version of C# that has pattern matching with none or only some of the recursive patterns working, as long as we make sure to "hold a place for them" in the way we design the places where patterns can occur. So C# 7 can have great pattern matching even if it doesn't yet have *all* of it. + \ No newline at end of file diff --git a/meetings/LDM-2016-04-12-22.md b/meetings/LDM-2016-04-12-22.md new file mode 100644 index 0000000000..c10935dda6 --- /dev/null +++ b/meetings/LDM-2016-04-12-22.md @@ -0,0 +1,225 @@ +# C# Design Notes for Apr 12-22, 2016 + +These notes summarize discussions across a series of design meetings in April on several topics related to tuples and patterns: +- Tuple syntax for non-tuple types +- Tuple deconstruction +- Tuple conversions +- Deconstruction and patterns +- Out vars and their scope + +There's still much more to do here, but lots of progress. +# Tuple syntax for other types + +We are introducing a new family of `System.ValueTuple<...>` types to support tuples in C#. However, there are already types that are tuple-like, such as `System.Tuple<...>` and `KeyValuePair<...>`. Not only are these often used throughout C# code, but the former is also what's targeted by F#'s tuple mechanism, which we'd want our tuple feature to interoperate well with. + +Additionally you can imagine allowing other types to benefit from some, or all, of the new tuple syntax. + +The obvious kinds of interop would be tuple construction and deconstruction. Since tuple literals are target typed, consider a tuple literal that is assigned to another tuple-like type: + +``` c# +System.Tuple t = (5, null); +``` + +We could think of this as calling `System.Tuple`'s constructor, or as a conversion, or maybe something else. Similarly, tuples will allow deconstruction and "decorated" names tracked by the compiler. Could we allow those on other types as well? + +There are several levels of support we could decide land on: +1. Only tuples are tuples. Other types are on their own. +2. Specific well-known tuple-like things are also tuples (probably `Tuple<...>` and `KeyValuePair<...>`). +3. An author of a type can make it tuple-like through certain API patterns +4. I can make anything work with tuple syntax without being the author of it +5. All types just work with it + +Level 2 would be enough to give us F# interop and improve the experience with existing APIs using `Tuple<...>` and `KeyValuePair<...>`. Option 3 could rely on any kind of declarations in the type, whereas option 4 would limit that to instance-method patterns that someone else could add through extension methods. + +It is hard to see how option 5 could work for deconstruction, but it might work for construction, simply by treating a tuple literal as an argument list to the type's constructor. One might consider it invasive that a type's constructor can be called without a `new` keyword or any mention of the type! On the other hand this might also be seen as a really nifty abbreviation. One problem would be how to use it with constructors with zero or one argument. So far we haven't opted to add syntax for 0-tuples and 1-tuples. + +We haven't yet decided which level we want to target, except we want to at least make `Tuple<...>` and `KeyValuePair<...>` work with tuple syntax. Whether we want to go further is a decision we probably cannot put off for a later version, since a later addition of capabilities might clash with user-defined conversions. +# Tuple deconstruction + +Whether deconstruction works for other types or not, we at least want to do it for tuples. There are three contexts in which we consider tuple deconstruction: +1. **Assignment:** Assign a tuple's element values into existing variables +2. **Declaration:** Declare fresh variables and initialize them with a tuple's element values +3. **Pattern matching:** Recursively apply patterns to each of a tuple's element values + +We would like to add forms of all three. +## Deconstructing assignments + +It should be possible to assign to existing variables, to fields and properties, array elements etc., the individual values of a tuple: + +``` c# +(x, y) = currentFrame.Crop(x, y); // x and y are existing variables +(a[x], a[x+1], a[x+2]) = GetCoordinates(); +``` + +We need to be careful with the evaluation order. For instance, a swap should work just fine: + +``` c# +(a, b) = (b, a); // Yay! +``` + +A core question is whether this is a new syntactic form, or just a variation of assignment expressions. The latter is attractive for uniformity reasons, but it does raise some questions. Normally, the type and value of an assignment expression is that of its left hand side after assignment. But in these cases, the left hand side has multiple values and types. Should we construct a tuple from those and yield that? That seems contrary to this being about _deconstructing_ not _constructing_ tuples! + +This is something we need to ponder further. As a fallback we can say that this is a new form of assignment _statement_, which doesn't produce a value. +## Deconstructing declarations + +In most of the places where local variables can be introduced and initialized, we'd like to allow deconstructing declarations - where multiple variables are declared, but assigned collectively from a single tuple (or tuple-like value): + +``` c# +(var x, var y) = GetCoordinates(); // locals in declaration statements +foreach ((var x, var y) in coordinateList) ... // iteration variables in foreach loops +from (x, y) in coordinateList ... // range variables in queries +M(out (var x, var y)); // tuple out parameters +``` + +For range variables in queries, this would depend on clever use of transparent identifiers over tuples. + +For out parameters this may require some form of post-call assignment, like VB has for properties passed to out parameters. That may or may not be worth it. + +For syntax, there are two general approaches: "Types-with-variables" or "types-apart". + +``` c# +// Types-with-variables: +(string first, string last) = GetName(); // Types specified +(var first, var last) = GetName(); // Types inferred +var (first, last) = GetName(); // Optional shorthand for all var + +// Types-apart: +(string, string) (first, last) = GetName(); // Types specified +var (first, last) = GetName(); // All types inferred +(var, var) (first, last) = GetName(); // Optional long hand for types inferred +``` + +This is mostly a matter of intuition and taste. For now we've opted for the types-with-variables approach, allowing the single `var` shorthand. One benefit is that this looks more similar to what we envision deconstruction in tuples to look like. Feedback may change our mind on this. + +Multiple variables won't make sense everywhere. For instance they don't seem appropriate or useful in using-statements. We'll work through the various declaration contexts one by one. +## Other deconstruction questions + +Should it be possible to deconstruct a longer tuple into fewer variables, discarding the rest? As a starting point, we don't think so, until we see scenarios for it. + +Should we allow optional tuple member names on the left hand side of a deconstruction? If you put them in, it would be checked that the corresponding tuple element had the name you expected: + +``` c# +(x: a, y: b) = GetCoordinates(); // Error if names in return tuple aren't x and y +``` + +This may be useful or confusing. It is also something that can be added later. We made no immediate decision on it. +# Tuple conversions + +Viewed as generic structs, tuple types aren't inherently covariant. Moreover, struct covariance is not supported by the CLR. And yet it seems entirely reasonable and safe that tuple values be allowed to be assigned to more accommodating tuple types: + +``` c# +(byte, short) t1 = (1, 2); +(int, int t2) = t1; // Why not? +``` + +The intuition is that tuple conversion should be thought of in a _pointwise_ manner. A tuple type is convertible (in a given manner) to another if each of their element types are pairwise convertible (in the same manner) to each other. + +However, if we are to allow this we need to build it into the language specifically - sometimes implementing tuple assignment as assigning the elements one-by-one, when the CLR doesn't allow the wholesale assignment of the tuple value. + +In essence we'd be looking at a situation similar to when we introduced nullable value types in C# 2. Those are implemented in terms of generic structs, but the language adds extensive special semantics to these generic structs, allowing operations - including covariant conversions - that do not automatically fall out from the underlying representation. + +This language-level relaxation comes with some subtle breaks that can happen on upgrade. Consider the following code, where C.dll is a C# 6 consumer of C# 7 libraries A.dll and B.dll: + +``` c# +A.dll: + +(int, long) Foo()... // ValueTuple Foo(); + +B.dll: + +void Bar(object o) +void Bar((int?, long?) t) + +C.dll: + +Bar(Foo()); +``` + +Because C# 6 has no knowledge of tuple conversions, it would pick the first overload of `Bar` for the call. However, when the owner of C.dll upgrades to C# 7, relaxed tuple conversion rules would make the second overload applicable, and a better pick. + +It is important to note that such breaks are esoteric. Exactly parallel examples could be constructed for when nullable value types were introduced; yet we never saw them in practice. Should they occur they are easy to work around. As long as the underlying type (in our case `ValueTuple<...>`) and the conversion rules are introduced at the same time, the risk of programmers getting them mixed in a dangerous manner is minimal. + +Another concern is that "pointwise" tuple conversions, just like nullable value types, are a pervasive change to the language, that affects many parts of the spec and implementation. Is it worth the trouble? After all it is pretty hard to come up with compelling examples where conversions between two tuple _types_ (as opposed to _from_ tuple literals or _to_ individual variables in a deconstruction) is needed. + +We feel that tuple conversions are an important part of the intuition around tuples, that they are primarily "groups of values" rather than "values in and of themselves". It would be highly surprising to developers if these conversions didn't work. Consider the baffling difference between these two pieces of code if tuple conversions didn't work: + +``` c# +(long, long) tuple = (1, 1); + +var tmp = (1, 1); +(long, long) tuple = tmp; // Doesn't work??!? +``` + +All in all we feel that pointwise tuple conversions are worth the effort. Furthermore it is crucial that they be added at the same time as the tuples themselves. We cannot add them later without significant breaking changes. +## Tuples vs ValueTuple + +In accordance with this philosophy we cannot say more about the relationship between language level tuple types and the underlying `ValueTuple<...>` types. + +Just like `Nullable` is equivalent to `T?`, so `ValueTuple` should be in every way equivalent to the unnamed `(T1, T2, T3)`. That means the pointwise conversions also work when tuple types are specified using the generic syntax. + +If the tuple is bigger than the limit of 7, the implementation will nest the "tail" as a tuple into the eighth element recursively. This nesting is visible by accessing the `Rest` field of a tuple, but that field is considered an implementation detail, and is hidden from e.g. auto-completion, just as the ItemX field names are hidden but allowed when a tuple has named elements. + +A well formed "big tuple" will have names `Item1` etc. all the way up to the number of tuple elements, even though the underlying type doesn't physically have those fields directly defined. The same goes for the tuple returned from the `Rest` field, only with the numbers "shifted" appropriately. All this says is that the tuple in the `Rest` field is treated the same as all other tuples. +# Deconstructors and patterns + +Whether or not we allow arbitrary values to opt in to the unconditional tuple deconstruction described above, we know we want to enable such positional deconstruction in recursive patterns: + +``` c# +if (o is Person("George", var last)) ... +``` + +The question is: how exactly does a type like `Person` specify how to be positionally deconstructed in such cases? There are a number of dimensions to this question, along with a number of options for each: +- Static or instance/extension member? +- `GetValues` method or new `is` operator? +- Return tuple or tuple out parameter or several out parameters? + +Selecting between these, we have to observe a number of different tradeoffs: +1. Overloadable or not? +2. Yields tuple or individual values? +3. Growth path to "active patterns"? +4. Can be applied to existing types without modifying them? + +Let's look at these in turn. +## Overloadability + +If the multiple extracted values are returned as a tuple from a method (whether static or instance) then that method cannot be overloaded. + +``` c# +public (string firstName, string lastName) GetValues() { ... } +``` + +The deconstructor is essentially canonical. That may not be a big deal from a usability perspective, but it does hamper the evolution of the type. If it ever adds another member and wants to enable access to it through deconstruction, it needs to _replace_ the deconstructor, it cannot just add a new overload. This seems unfortunate. + +A method that yields it results through one or more out parameters can be overloaded in C#. Also, for a new kind of user defined operator we can decide the overloading rule whichever way we like. For instance, conversion operators today can be overloaded on return type. +## Tuple or individual values + +If a deconstructor yields a tuple, then that confers special status to tuples for deconstruction. Essentially tuples would have their own built-in deconstruction mechanism, and all other types would defer to those by supplying a tuple. + +Even if we rely on multiple out parameters, tuples cannot just use the same mechanism. In order to do so, long tuples would need to be enhanced by the compiler with an implementation that hides the nested nature of such tuples. + +There doesn't seem to be any strong benefit to yielding a single tuple over multiple values (in out parameters). +## Growing up to active patterns + +There's a proposal where one type gets to specify deconstruction semantics for another, along even with logic to determine whether the pattern applies or not. We do not plan to support that in the first go-around, but it is worth considering whether the deconstruction mechanism lends itself to such an extension. + +In order to do so it would need to be static (so that it can specify behavior for an object of _another_ type), and would benefit from an out-parameter-based approach, so that the return position could be reserved for returning a boolean when the pattern is conditional. + +There is a lot of speculation involved in making such concessions now, and we could reasonably rely on our future selves to invent a separate specification mechanism for active patterns without us having to accommodate it now. +## Conclusion + +This was an exploration of the design space. The actual decision is left to a future meeting. +# Out vars and their scope + +We are in favor of reviving a restricted version of the declaration expressions that were considered for C# 6. This would allow methods following the TryFoo pattern to behave similarly to the new pattern-based is-expressions in conditions: + +``` c# +if (int.TryParse(s, out var i) && i > 0) ... +``` + +We call these "out vars", even though they are perfectly fine to specify a type. The scope rules for variables introduced in such contexts would be the same as variables coming from a pattern: they generally be in scope within all of the nearest enclosing statement, except when that is an if-statement, where they would not be in scope in the else-branch. + +On top of that there are some relatively esoteric positions we need to decide on. + +If an out var occurs in a _field initializer_, where should it be in scope? Just within the declarator where it occurs, not even in subsequent declarators of the same field declaration. + +If an out var occurs in a _constructor initializer_ (`this(...)` or `base(...)`) where should it be in scope? Let's not even allow that - there's no way you could have written equivalent code yourself. diff --git a/meetings/LDM-2016-05-03,04.md b/meetings/LDM-2016-05-03,04.md new file mode 100644 index 0000000000..04f7956fc9 --- /dev/null +++ b/meetings/LDM-2016-05-03,04.md @@ -0,0 +1,109 @@ +# C# Design Notes for May 3-4, 2016 + +This pair of meetings further explored the space around tuple syntax, pattern matching and deconstruction. +1. Deconstructors - how to specify them +2. Switch conversions - how to deal with them +3. Tuple conversions - how to do them +4. Tuple-like types - how to construct them + +Lots of concrete decisions, that allow us to make progress on implementation. +# Deconstructors + +In [#11031](https://github.com/dotnet/roslyn/issues/11031) we discussed the different contexts in which deconstruction should be able to occur, namely deconstructing _assignment_ (into existing variables), _declaration_ (into freshly declared local variables) and _patterns_ (as part of applying a recursive pattern). + +We also explored the design space of how exactly "deconstructability" should be specified for a given type, but left the decision open - until now. Here's what we decided - and why. We'll stick to these decisions in initial prototypes, but as always are willing to be swayed by evidence as we roll them out and get usage. + +**_Deconstruction should be specified with an instance (or extension) method**_. This is in keeping with other API patterns added throughout the history of C#, such as `GetEnumerator`, `Add`, and `GetAwaiter`. The benefit is that this leads to a relatively natural kind of member to have, and it can be specified with an extension method so that existing types can be augmented to be deconstructable outside of their own code. + +The choice limits the ability of the pattern to later grow up to facilitate "active patterns". We aren't too concerned about that, because if we want to add active patterns at a later date we can easily come up with a separate mechanism for specifying those. + +**_The instance/extension method should be called `Deconstruct`**_. We've been informally calling it `GetValues` for a while, but that name suffers from being in too popular use already, and not always for a similar purpose. This is a decision we're willing to alter if a better name comes along, and is sufficiently unencumbered. + +**_The Deconstruct method "returns" the component values by use of individual out parameters**_. This choice may seem odd: after all we're adding a perfectly great feature called tuples, just so that you can return multiple values! The motivation here is primarily that we want `Deconstruct` to be overloadable. Sometimes there are genuinely multiple ways to deconstruct, and sometimes the type evolves over time to add more properties, and as you extend the `Deconstruct` method you also want to leave an old overload available for source and binary compat. + +This one does nag us a little, because the declaration form with tuples is so much simpler, and would be sufficient in a majority of cases. On the other hand, this allows us to declare decomposition logic _for_ tuples the same way as for other types, which we couldn't if we depended on tuples for it! + +Should this become a major nuisance (we don't think so) one could consider a hybrid approach where both tuple-returning and out-parameter versions were recognized, but for now we won't. + +All in all, the deconstructor pattern looks like one of these: + +``` c# +class Name +{ + public void Deconstruct(out string first, out string last) { first = First; last = Last; } + ... +} +// or +static class Extensions +{ + public static void Deconstruct(this Name name, out string first, out string last) { first = name.First; last = name.Last; } +} +``` +# Switch conversions + +Switch statements today have a wrinkle where they will apply a unique implicit conversion from the switched-on expression to a (currently) switchable type. As we expand to allow switching on any type, this may be confusing at times, but we need to keep it at least in some scenarios, for backwards compatibility. + +``` c# +switch (expr) // of some type Expression, which "cleverly" has a user defined conversion to int for evaluation +{ + case Constant(int i): ... // Won't work, though Constant derives from Expression, because expr has been converted to int + ... +} +``` + +Our current stance is that this is fringe enough for us to ignore. If you run into such a conversion and didn't want it, you'll have to work around it, e.g. by casting your switch expression to object. + +If this turns out to be more of a nuisance we may have to come up with a smarter rule, but for now we're good with this. +# Tuple conversions + +In #11031 we decided to add tuple conversions, that essentially convert tuples whenever their elements convert - unlike the more restrictive conversions that follow from `ValueTuple<...>` being a generic struct. In this we view nullable value types as a great example of how to imbue a language-embraces special type with more permissive conversion semantics. + +As a guiding principle, we would like tuple conversions to apply whenever a tuple can be deconstructed and reassembled into the new tuple type: + +``` c# +(string, byte) t1 = ...; +(object, int) t2 = t1; // Allowed, because the following is: +(var a, var b) = t1; // Deconstruct, and ... +(object, int) t2 = (a, b); // reassemble +``` + +One problem is that nullable value type conversions are rather complex. They affect many parts of the language. It'd be great if we could make tuple conversions simpler. There are two principles we can try to follow: +1. A tuple conversion is a specific _kind_ of conversion, and it allows specific _kinds_ of conversions on the elements +2. A tuple conversion works in a given setting if all of its element conversions would work in that setting + +The latter is more general, more complex and possibly ultimately necessary. However, somewhat to our surprise, we found a definition along the former principle that we cannot immediately poke a hole in: + +> An _implicit tuple conversion_ is a standard conversion. It applies between two tuple types of equal arity when there is _any_ implicit conversion between each corresponding pair of types. + +(Similarly for explicit conversions). + +The interesting part here is that it's a standard conversion, so it is able to be composed with user defined conversions. Yet, its elements are allowed to perform their own user defined conversions! It feels like something could go wrong here, with recursive or circular application of user defined conversions, but we haven't been able to pinpoint an example. + +A definition like this would be very desirable, because it won't require so much special casing around the spec. + +We will try to implement this and see if we run into problems. +# Tuple-like construction of non-tuple types + +We previously discussed to what extent non-tuple types should benefit from the tuple syntax. We've already decided that the deconstruction syntax applies to any type with a deconstructor, not just tuples. So what about construction? + +The problem with allowing tuple literal syntax to construct any type is that _all_ types have constructors! There's no opt-in. This seems too out of control. Furthermore, it doesn't look intuitive that any old type can be "constructed" with a tuple literal: + +``` c# +Dictionary d = (16, EqualityComparer.Default); / Huh??? +``` + +This only seems meaningful if the constructor arguments coming in through a "tuple literal" are actually the constituent data of the object being created. + +Finally, we don't have syntax for 0 and 1-tuples, so unless we add that, this would only even work when there's more than one constructor argument to the target type. + +All in all, we don't think tuple literals should work for any types other than the built-in tuples. Instead, we want to brush off a feature that we've looked at before; the ability to omit the type from an object creation expression, when there is a target type: + +``` c# +Point p = new (3, 4); // Same as new Point(3, 4) +List l1 = new (10); // Works for 0 or 1 argument +List l2 = new (){ 3, 4, 5 }; // Works with object/collection initializers, but must have parens as well. +``` + +Syntactically we would say that an object creation expression can omit the type when it has a parenthesized argument list. In the case of object and collection initializers, you cannot omit both the type and the parenthesized argument list, since that would lead to ambiguity with anonymous objects. + +We think that this is promising. It is generally useful, and it would work nicely in the case of existing tuple-like types such as `System.Tuple<...>` and `KeyValuePair<...>`. diff --git a/meetings/LDM-2016-05-10.md b/meetings/LDM-2016-05-10.md new file mode 100644 index 0000000000..e4ca8de85c --- /dev/null +++ b/meetings/LDM-2016-05-10.md @@ -0,0 +1,52 @@ +# C# Language Design Notes for May 10, 2016 + +In this meeting we took a look at the possibility of adding new kinds of extension members, beyond extension methods. +## Extension members + +Ever since we added extension methods in C# 3.0, there's been a consistent ask to add a similar feature for other kinds of function members. While "instance methods" are clearly the most important member kind to "extend with" from the outside, it seems arbitrary and limiting not to have e.g. properties, constructors or static members. + +Unfortunately, the declaration syntax for extension methods is sort of a local optimum. The static method with the extra `this` parameter works great for methods: It is low cost, and it comes with an obvious disambiguation syntax. However, this approach doesn't carry over well to other kinds of members. A property with an extra `this` parameter? And how would extension static members attach to their type - cannot be through a `this` parameter representing an instance! As we were designing C# 4 we tried very hard to answer these questions and design an "extension everything" feature that was a syntactic extension (no pun intended) of extension methods. We failed, creating a succession of monsters until we gave up. + +The other option is to take a step back, and do a different style of design. Most proposals nowadays do this, and the idea actually goes all the way back to debates we had when adding extension methods in the first place: instead of the extension _members_ saying what they extend, maybe they should be grouped into "extensions"; class-like groups of extension methods that all extend the same type. With the enclosing "extension class" doing all the extension, the members can just be declared with their normal syntax: + +``` c# +class Person +{ + public string Name { get; } + public Person(string name) { Name = name; } +} + +extension class Enrollee extends Person +{ + static Dictionary enrollees = new (); // static field + public void Enroll(Professor supervisor) { enrollees[this] = supervisor; } // instance method + public Professor Supervisor => enrollees.TryGetValue(this, out var supervisor) ? supervisor : null; // instance property + public static ICollection Students => enrollees.Keys; // static property + public Person(string name, Professor supervisor) : this(name) { this.Enroll(supervisor); } // constructor +} +``` + +Issue [#11159](https://github.com/dotnet/roslyn/issues/11159) summarizes the recent proposals along these lines. + +This syntactic approach has clear advantages - writing an extension member is as simple as writing a normal member. It does have some challenges, too. One is, what does it compile into? Another is, how do you disambiguate when several extension members are in scope? Those are trivially answered by the current extension methods, but need more elaborate answers here. + +Looking at our current BCL libraries, it is clear that there are many places where layering is poor. Essentially, there's a trade off between layering (separating dependencies) and discoverability (offering members _on_ the important types). _If_ you have a file system, you want to offer a constructor overload that takes a file name. Extension constructors (for instance) address this. + +Also, extension members allow putting concrete functionality on interfaces, and on specific instances of generic types. + +We think that the most useful and straightforward kinds of extension methods to add would be instance and static versions of methods, properties and indexers, as well as static fields. We don't think instance fields make sense - where would they live? The answer to that is at best complicated, and probably not efficient. + +Extension constructors would also be useful. There's a bit more of a design space here, depending on whether you want them to be just like instance constructors (which would require the specific extended type to already have a constructor to delegate to), or more like factories (which could be added to interfaces etc. and produce instances of more derived types). + +Extension operators are in a bit of a gray zone. Some of them, like arithmetic operators, probably make sense. Equality operators are hard, because everyone's already got one, so the "built-in" would always shadow. Implicit conversion operators are especially problematic, because their invocation doesn't have an associated syntax, so it's very subtle when they are applied, and there's no easy way to choose _not_ to do it. + +Implementation-wise, the extension class would probably turn into a static class, and the extension members would probably be represented as static methods, with the most obvious mapping we can find. There's a case for implementing extension properties via "indexed properties" which are supported in IL (and VB), though maybe we'd prefer mapping to something that could be written directly in C#. + +The name of the extension would be the name of the static class, and can also be used in disambiguation, for which we would need a syntax. Maybe existing syntax, such as `((Enrollee)person).Supervisor` or `(person as Enrollee).Supervisor` could be used when disambiguating instance members or operators, whereas static members would just be accessed directly off of the generated static class, as in `Enrollee.Students`. + +Arguably, consumption is more important than production. In this case, that is an argument both for and against introducing a new syntactic approach to declaring extension members: On the one hand, it makes it less of a problem if we effectively deprecate the existing syntax, on the other it makes it less compelling to add syntax for it, instead of maybe some attribute-based approach, or other less syntax-heavy solutions. + +For extension properties, people often point to `Enumerable.Count(IEnumerable src)` as an example of why they want them, but that's a terrible example, because it isn't efficient. Even so, there's probably a good need for these - the Roslyn code base for instance has many little extension methods on top of interfaces, that "should" have been properties. `Kind()` is a perfect example. +## Conclusion + +We think this is worth pursuing further. We will come back to it in future design meetings to start ironing out the details. diff --git a/meetings/LDM-2016-07-12.md b/meetings/LDM-2016-07-12.md new file mode 100644 index 0000000000..a0ffd6b57a --- /dev/null +++ b/meetings/LDM-2016-07-12.md @@ -0,0 +1,67 @@ +# C# Language Design Notes for Jul 12, 2016 +## Agenda + +Several design details pertaining to tuples and deconstruction resolved. +# All or nothing with element names? + +There's certainly a case for partial names on literals - sometimes you want to specify the names of _some_ of the elements for clarity. These names have to fit names in the tuple type that the literal is being converted to: + +``` c# +List<(string firstName, string lastName, bool paid)> people = ...; + +people.Add(("John", "Doe", paid: true)); +``` + +Do we also want to allow partial names in _types_? + +Yes we do. We don't have strong arguments against it, and generality calls for it. It's likely that there are scenarios, and not allowing will feel arbitrary. "The `bool` deserves a clarifying name, but the `TypeSymbol` is clear". + +``` c# +var t = (1, y: 2); // infers (int, int y) +(int x, int) t = (1, 2); +``` + +As a reminder, we distinguish between leaving out the name and explicitly specifying `Item1`, `Item2`, etc. You get a warning if you use `Item1`... in literals if they are not explicitly there in the target type. (Warning)` + +``` c# +(int, int) t = (Item1: 1, Item2: 2); // Warning, names are lost +``` + +For the editor experience we imagine offering completion of a named element in the corresponding position of a tuple literal. +# ITuple + +``` c# +interface ITuple +{ + int Size; + object this[int i] { get; } +} +``` + +`ITuple` is really an interface for dynamically checking for deconstructability. Whether we should add `ITuple` now depends on whether we think we want to allow recursive patterns to do that, once we add them. + +We _do_ think we want that. + +We cannot use an existing collection interface (such as `IReadOnlyCollection`) because we want to be able to distinguish between deconstructable objects and collections (which might one day get their own pattern). It is fine for a class to implement both. + +`ValueTuple<...>` should of course implement `ITuple`, and the translation of long tuples should work. + +The Framework team should chime in on the specific shape and names on the interface. Is `ITuple` the right name, or is it too narrow? Is `IDeconstructable` more to the point or is it too long? Should it be `Size` or `Length` or `Count`? +# var when var type exists + +``` c# +class var {} +var (x, y) = e; +``` + +People use this trick to block the use of `var`, and we should let them, even though we disagree with the practice. So even though a type isn't valid in that position, we will let the presence of a `var` type turn this into an error. +# var method + +What if there's a _method_ called `var`? + +``` c# +ref int var(int x, int y); +var(x, y) = e; // deconstruction or call? +``` + +Here there is no prior art, so we will always interpret the `var` to mean a deconstructing declaration. If you wanted the method call, parenthesize the call or use @. diff --git a/meetings/LDM-2016-07-13.md b/meetings/LDM-2016-07-13.md new file mode 100644 index 0000000000..880c2b2cce --- /dev/null +++ b/meetings/LDM-2016-07-13.md @@ -0,0 +1,115 @@ +# C# Language Design Notes for Jul 13, 2016 +## Agenda + +We resolved a number of questions related to tuples and deconstruction, and one around equality of floating point values in pattern matching. +# Handling conflicting element names across type declarations + +For tuple element names occurring in partial type declarations, we will require the names to be the same. + +``` c# +partial class C : IEnumerable<(string name, int age)> { ... } +partial class C : IEnumerable<(string fullname, int)> { ... } // error: names must be specified and the same +``` + +For tuple element names in overridden signatures, and when identity convertible interfaces conflict, there are two camps: +1. Strict: the clashes are _disallowed_, on the grounds that they probably represent programmer mistakes + - when overriding or implementing a method, tuple element names in parameter and return types must be preserved + - it is an error for the same generic interface to be inherited/implemented twice with identity convertible type arguments that have conflicting tuple element names +2. Loose: the clashes are _resolved_, on the grounds that we shouldn't (and don't otherwise) dictate such things (e.g. with parameter names) + - when overriding or implementing a method, different tuple element names can be used, and on usage the ones from the most derived statically known type win, similar to parameter names + - when interfaces with different tuple element names coincide, the conflicting names are elided, similar to best common type. + +``` c# +interface I1 : IEnumerable<(int a, int b)> {} +interface I2 : IEnumerable<(int c, int d)> {} +interface I3 : I1, I2 {} // what comes out when you enumerate? +class C : I1 { public IEnumerator<(int e, int f)> GetEnumerator() {} } // what comes out when you enumerate? +``` + +We'll go with the strict approach, barring any challenges we find with it. We think helping folks stay on the straight and narrow here is the most helpful. If we discover that this is prohibitive for important scenarios we haven't though of, it will be possible to loosen the rules in later releases. +# Deconstruction of tuple literals + +Should it be possible to deconstruct tuple literals directly, even if they don't have a "natural" type? + +``` c# +(string x, byte y, var z) = (null, 1, 2); +(string x, byte y) t = (null, 1); +``` + +Intuitively the former should work just like the latter, with the added ability to handle point-wise `var` inference. + +It should also work for deconstructing assignments: + +``` c# +string x; +byte y; + +(x, y) = (null, 1); +(x, y) = (y, x); // swap! +``` + +It should all work. Even though there never observably is a physical tuple in existence (it can be thought of as a series of point-wise assignments), semantics should correspond to introducing a fake tuple type, then imposing it on the RHS. + +This means that the evaluation order is "breadth first": +1. Evaluate the LHS. That means evaluate each of the expressions inside of it one by one, left to right, to yield side effects and establish a storage location for each. +2. Evaluate the RHS. That means evaluate each of the expressions inside of it one by one, left to right to yield side effects +3. Convert each of the RHS expressions to the LHS types expected, one by one, left to right +4. Assign each of the conversion results from 3 to the storage locations found in 1. + +This approach ensures that you can use the feature for swapping variables `(x, y) = (y, x);`! +# var in tuple types? + +``` c# +(var x, var y) = GetTuple(); // works +(var x, var y) t = GetTuple(): // should it work? +``` + +No. We will keep `var` as a thing to introduce local variables only, not members, elements or otherwise. For now at least. +# Void as result of deconstructing assignment? + +We decided that deconstructing assignment should still be an expression. As a stop gap we said that its type could be void. This still grammatically allows code like this: + +``` c# +for (... ;; (current, next) = (next, next.Next)) { ... } +``` + +We'd like the result of such a deconstructing assignment to be a tuple, not void. This feels like a compatible change we can make later, and we are open to it not making it into C# 7.0, but longer term we think that the result of a deconstructing assignment should be a tuple. Of course a compiler should feel free to not actually construct that tuple in the overwhelming majority of cases where the result of the assignment expression is not used. + +The normal semantics of assignment is that the result is the value of the LHS after assignment. With this in mind we will interpret the deconstruction in the LHS as a tuple: it will have the values and types of each of the variables in the LHS. It will not have element names. (If that is important, we could add a syntax for that later, but we don't think it is). +# Deconstruction as conversion and vice versa + +Deconstruction and conversion are similar in some ways - deconstruction feels a bit like a conversion to a tuple. Should those be unified somehow? + +We think no. the existence of a `Deconstruct` method should not imply conversion: implicit conversion should always be explicitly specified, because it comes with so many implications. + +We could consider letting user defined implicit conversion imply `Deconstruct`. It leads to some convenience, but makes for a less clean correspondence with consumption code. + +Let's keep it separate. If you want a type to be both deconstructible and convertible to tuple, you need to specify both. +# Anonymous types + +Should they implement `Deconstruct` and `ITuple`, and be convertible to tuples? + +No. There are no really valuable scenarios for moving them forward. Wherever that may seem desirable, it seems tuples themselves would be a better solution. +# Wildcards in deconstruction + +We should allow deconstruction to feature wildcards, so you don't need to specify dummy variables. + +The syntax for a wildcard is `*`. This is an independent feature, and we realize it may be bumped to post 7.0. +# compound assignment with distributive semantics + +``` c# +pair += (1, 2); +``` + +No. +# Switch on double + +What equality should we use when switching on floats and doubles? +- We could use `==` - then `case NaN` wouldn't match anything. +- We could use `.Equals`, which is similar except treating `NaN`s as equal. + +The former struggles with "at what static type"? The latter is defined independently of that. The former would equate 1 and 1.0, as well as byte 1 and int 1 (if applied to non-floating types as well). The latter won't. + +With the latter we'd feel free to optimize the boxing and call of Equals away with knowledge of the semantics. + +Let's do `.Equals`. diff --git a/meetings/LDM-2016-07-15.md b/meetings/LDM-2016-07-15.md new file mode 100644 index 0000000000..1efc4e8baf --- /dev/null +++ b/meetings/LDM-2016-07-15.md @@ -0,0 +1,120 @@ +# C# Design Language Notes for Jul 15, 2016 +## Agenda + +In this meeting we took a look at what the scope rules should be for variables introduced by patterns and out vars. +# Scope of locals introduced in expressions + +So far in C#, local variables have only been: +1. Introduced by specific kinds of statements, and +2. Scoped by specific kinds of statements + +`for`, `foreach` and `using` statements are all able to introduce locals, but at the same time also constitute their scope. Declaration statements can introduce local variables into their immediate surroundings, but those surroundings are prevented by grammar from being anything other than a block `{...}`. So for most statement forms, questions of scope are irrelevant. + +Well, not anymore! Expressions can now contain patterns and out arguments that introduce fresh variables. For any statement form that can contain expressions, we therefore need to decide how it relates to the scope of such variables. +## Current design + +Our default approach has been fairly restrictive: +- All such variables are scoped to the nearest enclosing statement (even if that is a declaration statement) +- Variables introduced in the condition of an `if` statement aren't in scope in the `else` clause (to allow reuse of variable names in nested `else if`s) + +This approach caters to the "positive" scenarios of `is` expressions with patterns and invocations of `Try...` style methods with out parameters: + +``` c# +if (o is bool b) ...b...; // b is in scope +else if (o is byte b) ...b...; // fine because bool b is out of scope +...; // no b's in scope here +``` + +It doesn't handle unconditional uses of out vars, though: + +``` c# +GetCoordinates(out var x, out var y); +...; // x and y not in scope :-( +``` + +It also fits poorly with the "negative" scenarios embodied by what is sometimes called the "bouncer pattern", where a method body starts out with a bunch of tests (of parameters etc.) and jumps out if the tests fail. At the end of the test you can write code at the highest level of indentation that can assume that all the tests succeeded: + +``` +void M(object o) +{ + if (o == null) throw new ArgumentNullException(nameof(o)); + ...; // o is known not to be null +} +``` + +However, the strict scope rules above make it intractable to extend the bouncer pattern to use patterns and out vars: + +``` c# +void M(object o) +{ + if (!(o is int i)) throw new ArgumentException("Not an int", nameof(o)); + ...; // we know o is int, but i is out of scope :-( +} +``` +## Guard statements + +In Swift, this scenario was found so important that it earned its own language feature, `guard`, that acts like an inverted `if`, except that a) variables introduced in the conditions are in scope after the `guard` statement, and b) there must be an `else` branch that leaves the enclosing scope. In C# it might look something like this: + +``` c# +void M(object o) +{ + guard (o is int i) else throw new ArgumentException("Not an int", nameof(o)); // else must leave scope + ...i...; // i is in scope because the guard statement is specially leaky +} +``` + +A new statement form seems like a heavy price to pay. And a guard statement wouldn't deal with non-error bouncers that correct the problem instead of bailing out: + +``` c# +void M(object o) +{ + if (!(o is int i)) i = 0; + ...; // would be nice if i was in scope and definitely assigned here +} +``` + +(In the bouncer analogy I guess this is equivalent to the bouncer lending the non-conforming customer a tie instead of throwing them out for not wearing one). +## Looser scope rules + +It would seem better to address the scenarios and avoid a new statement form by adopting more relaxed scoping rules for these variables. + +_How_ relaxed, though? + +**Option 1: Expression variables are only scoped by blocks** + +This is as lenient as it gets. It would create some odd situations, though: + +``` c# +for (int i = foo(out int j);;); +// j in scope but not i? +``` + +It seems that these new variables should at least be scoped by the same statements as old ones: + +**Option 2: Expression variables are scoped by blocks, for, foreach and using statements, just like other locals:** + +This seems more sane. However, it still leads to possibly confusing and rarely useful situations where a variable "bleeds" out many levels: + +``` c# +if (...) + if (...) + if (o is int i) ...i... +...; // i is in scope but almost certainly not definitely assigned +``` + +It is unlikely that the inner `if` intended `i` to bleed out so aggressively, since it would almost certainly not be useful at the outer level, and would just pollute the scope. + +One could say that this can easily be avoided by the guidance of using curly brace blocks in all branches and bodies, but it is unfortunate if that changes from being a style guideline to having semantic meaning. + +**Option 3: Expression variables are scoped by blocks, for, foreach and using statements, as well as all embedded statements:** + +What is meant by an embedded statement here, is one that is used as a nested statement in another statement - except inside a block. Thus the branches of an `if` statement, the bodies of `while`, `foreach`, etc. would all be considered embedded. + +The consequence is that variables would always escape the condition of an `if`, but never its branches. It's as if you put curlies in all the places you were "supposed to". +## Conclusion + +While a little subtle, we will adopt option 3. It strikes a good balance: +- It enables key scenarios, including out vars for non-`Try` methods, as well as patterns and out vars in bouncer if-statements. +- It doesn't lead to egregious and counter-intuitive multi-level "spilling". + +It does mean that you will get more variables in scope than the current restrictive regime. This does not seem dangerous, because definite assignment analysis will prevent uninitialized use. However, it prevents the variable names from being reused, and leads to more names showing in completion lists. This seems like a reasonable tradeoff. diff --git a/meetings/LDM-2016-08-24.md b/meetings/LDM-2016-08-24.md new file mode 100644 index 0000000000..5712c3958e --- /dev/null +++ b/meetings/LDM-2016-08-24.md @@ -0,0 +1,69 @@ +C# Language Design Meeting, Aug 24, 2016 +======================================== + +## Agenda + +After a meeting-free period of implementation work on C# 7.0, we had a few issues come up for resolution. + +1. What does it take to be task-like? +2. Scope of expression variables in initializers + +# Task-like types + +The generalization of async return types requires the return type of an async method to provide an implementation for the "builder object" that the method body's state machine interacts with when it awaits, and when it produces the returned "task-like" value. + +How should it provide this implementation? There are a couple of options: + +1. A `GetAsyncMethodBuilder` method on the task-like type following a specific compiler-recognized pattern +2. 1 + a modifier on the type signifying "buy-in" to being an async type +3. An attribute that encodes the builder type + +We like the attribute approach, because this is really a metadata concern. The attribute sidesteps issues with accessibility (is the type only task-like when the `GetAsyncMethodBuilder` method is accessible?), and doesn't pollute the member set of the type. Also it works on interfaces. For instance, it could conceivably apply to WinRT's `IAsyncAction` etc. + +We only expect very few people to ever build their own task-like types. We may or may not choose to ever actually *ship* the attribute we will use. Implementers can just roll their own internal implementation. The compiler only looks for the name - which will be `System.Runtime.CompilerServices.AsyncBuilderAttribute`. + +There are a couple of options for what the attribute should contain: + +1. Nothing - it is just a marker, and there is still a static method on the task-like type +2. The name of a method to call on the task-like type +3. The builder type itself +4. A static type for *getting* the builder + +Option 1 and 2 we've already ruled out, because we don't want to depend on accessible members on the task-like type itself. + +Option 3 would require a `Type` argument to the constructor that is either non-generic (for void-yielding or non-generic result-yielding task-like types) or an open generic type with one type parameter (for generic result-yielding task-like types) +Option 4 is essentially a "builder builder", and would let us have a less dirty relationship between type parameters on the builder and task-like: + +``` c# +static class ValueTaskBuilderBuilder +{ + ValueTaskBuilder GetBuilder(ValueTask dummy); +} +``` + +That is probably a bridge too far, so we will go with Option 3. + +The builder type given in the attribute is required to have a static, accessible, non-generic Create method. + +This would work for `IAsyncAction` etc. from WinRT, assuming that a builder is implemented for them, and an attribute placed on them. + +The proposal to allow the builder to be referenced from the async method is attractive, but something for another day. + +# Scope of expression variables in initializers + +``` c# +int x = M(out int t), y = t; +``` + +If this is a local declaration, `t` is in scope after the declaration. If this is a _field_ declaration, though, should it be? It's problematic if it is, and inconsistent if it isn't! Also, if we decide one way or another, we can never compatibly change our mind on it. + +Let's block this off and not allow expression variables to be introduced in field initializers! It's useless today, and we should save the scoping decisions for when future features give us a use case. + +``` c# +public C() : base(out int x) +{ + // use x? +} +``` + +A variable declared in a `this` or `base` initializer should be in scope in the whole constructor body. diff --git a/meetings/LDM-2016-09-06.md b/meetings/LDM-2016-09-06.md new file mode 100644 index 0000000000..aa4177413c --- /dev/null +++ b/meetings/LDM-2016-09-06.md @@ -0,0 +1,22 @@ +# C# Language Design Notes for Sep 6, 2016 + +## Agenda + +1. How do we select `Deconstruct` methods? + +# How do we select Deconstruct methods? + +`(int x, var y) = p` cannot just turn into `p.Deconstruct(out int x, out var y)`, because we want it to find a `Deconstruct` method with a more specific type than `int`, e.g. `byte`. + +We should look only at the arity of the `Deconstruct` method. If there's more than one with the given arity, we fail. If necessary, we will then translate this into passing temporary variables to the `Deconstruct` method, instead of the ones declared in the deconstruction. E.g., if `p` has + +``` C# +void Deconstruct(out byte x, out byte y) ...; +``` + +We would translate it equivalently to: + +``` c# +p.Deconstruct(out byte __x, out byte __y); +(int x, int y) = (__x, __y); +``` \ No newline at end of file diff --git a/meetings/LDM-2016-10-18.md b/meetings/LDM-2016-10-18.md new file mode 100644 index 0000000000..37d856e7dd --- /dev/null +++ b/meetings/LDM-2016-10-18.md @@ -0,0 +1,68 @@ +C# Language Design Meeting Notes, Oct 18, 2016 +============================================== + + +## Agenda + +Go over C# 7.0 features one last (?) time to make sure we feel good about them and address remaining language-level concerns. + +1. Wildcard syntax +2. Design "room" between tuples and patterns +3. Local functions +4. Digit separators +5. Throw expressions +6. Tuple name mismatch warnings +7. Tuple types in `new` expressions + + +# Pattern matching + +## Wildcards +If we want to reopen the discussion of using `_`, and it doesn't make C# 7.0, we may find ourselves wanting to block off use of `_` as an ordinary identifier in the new declaration contexts (patterns, deconstruction, out vars). Let's front load the final design of wildcards to decide if anything needs to happen here. + +## Design "room" between tuples and pattern matching + +`case (int, int) x:` is not currently allowed, in order to leave design space. More specifically we want this to mean a recursive pattern, rather than just a tuple type, once we get to recursive patterns. We're good with leaving this an error in the meantime, even though it may occasionally be puzzling to developers. + + +# Local functions + +We want to ideally allow any expression or set of statements to be lifted out in a local method. There are two ways in which this cannot be realized: + +1. assignment to readonly fields in constructors - the runtime disallows those assignments if we lift them out to methods. +2. async methods and iterators - they aren't done executing when they return, so they can't generally contribute to definite assignment of enclosing variables. For async local functions we do recognize assignments that happen before the first await. Is this too subtle? Maybe, but it's fine to keep it. + + +# Digit separators + +We don't allow leading or trailing underbars - they have to be *between* digits: they are digit *separators* after all! We think this is fine, but if we hear feedback to the contrary we can try to relax it later. + + +# Throw expressions + +They are allowed as expression bodies, as the second operand of `??`, and as the second and third operand of `?:`. They are not allowed in `&&` and `||`, and cannot be parenthesized. We think this is a fine place to land. + + +# Tuples + +## Name mismatch warnings + +We don't currently warn about names moving to a different position. Should we? + +``` c# +(int first, int last) M() ... + +(int last, int first) t = M(); // Oops! +``` + +Ideally yes. There are a lot of weird cases that would be hard to track down, though. Let's get the obvious ones. Essentially where we do implicit conversions we would check and warn. + + +## Use of tuples in `new` + +You cannot `new` up a tuple type. However, we should certainly allow arrays of tuples to be created. It is probably also fine to keep allowing `new`ing of nullable tuple types: + +``` c# +var array = new (int x, int y)[10]; // Absolutely +var nullable = new (int x, int y)?(); // Why not? +``` diff --git a/meetings/LDM-2016-10-25,26.md b/meetings/LDM-2016-10-25,26.md new file mode 100644 index 0000000000..767ca5ca85 --- /dev/null +++ b/meetings/LDM-2016-10-25,26.md @@ -0,0 +1,150 @@ +C# Language Design Notes for Oct 25 and 26, 2016 +================================================ + +Agenda +------ + +- Declaration expressions as a generalizing concept +- Irrefutable patterns and definite assignment +- Allowing tuple-returning deconstructors +- Avoiding accidental reuse of out variables +- Allowing underbar as wildcard character + + +Declaration expressions +======================= + +In C# 6.0 we embraced, but eventually abandoned, a very general notion of "declaration expressions" - the idea that a variable could be introduced *as* an expression `int x`. + +The full generality of that proposal had some problems: + +* A variable introduced like `int x` is unassigned and therefore unusable in most contexts. It also leads to syntactic ambiguities +* We therefore allowed an optional initializer `int x = e`, so that it could be used elsewhere. But that lead to weirdness around when `= e` meant assignment (the result of which is a *value*) and when it meant initialization (the result of which is a *variable* that can be assigned to or passed by ref). + +However, there's value in looking at C#'s new deconstruction and out variable features through the lens of declaration expressions. + +Currently we have + +* Deconstructing *assignments* of the form `(x, y) = e` +* Deconstructing *declarations* of the form `(X x, Y y) = e` +* Out variables of the form `M(out X x)` + +This calls for generalization. What if we said that + +* Tuple expressions `(e1, e2)` can be lvalues if all their elements are lvalues, and will cause deconstruction when assigned to +* There are declaration expressions `X x` that can only occur in positions where an unassigned variable is allowed, that is + * In or as part of the left hand side of an assignment + * In or as part of an out argument + +Then all the above features - and more - can be expressed in terms of combinations of the two: + +* Deconstructing assignments are just a tuple on the left hand side of an assignment +* Deconstructing declarations are just deconstructing assignments where all the nested variables are declaration expressions +* Out variables are just declaration expressions passed as an out argument + +Given the short time left for fixes to C# 7.0 we could still keep it to these three special cases for now. However, if those restrictions were later to be lifted, it would lead to a number of other things being expressible: + +* Using a "deconstructing declaration" as an expression (today it is a statement) +* Mixing existing and new variables in a deconstructing assignment `(x, int y) = e` +* deconstruction in out context `M(out (x, y))` +* single declaration expression on the left hand side of assignment `int x = e` + +The work involved in moving to this model *without* adding this functionality would be to modify the representation in the Roslyn API, so that it can be naturally generalized later. + +Conclusion +---------- + +Let's re-cast the currently planned C# 7.0 features in turns of declaration expressions and tuple expressions, and then plan to later add some or all of the additional expressiveness this enables. + + +Irrefutable patterns +==================== + +Some patterns are "irrefutable", meaning that they are known by the compiler to be always fulfilled. We currently make very limited use of this property in the "subsumption" analysis of switch cases. But we could use it elsewhere: + +``` c# +if (GetInt() is int i) { ... } +UseInt(i); // Currently an error. We could know that i is definitely assigned here +``` + +This might not seem to be of much value - why are you applying a pattern if it never fails? But in a future with recursive patterns, this may become more useful: + +``` c# +if (input is Assignment(Expression left, var right) && left == right) { ... } +... // condition failed, but left and right are still assigned +``` + +Conclusion +---------- + +This seems harmless, and will grow more useful over time. It's a small tweak that we should do. + + +Tuple-returning deconstructors +============================== + +It's a bit irksome that deconstructors must be written with out parameters. This is to allow overloading on arity, but in practice most types would only declare one. We could at least optionally allow one of them to be defined with a return tuple instead: + +``` c# +(int x, int y) Deconstruct() => (X, Y); +``` + +Instead of: +``` c# +void Deconstruct(out int x, out int y) => (x, y) = (X, Y); +``` + +Evidence is inconclusive as to which is more efficient: it depends on circumstances. + +Conclusion +---------- + +Not worth it. + + +Definite assignment for out vars +================================ + +With the new scope rules, folks have been running into this: + +``` c# +if (int.TryParse(s1, out var i)) { ... i ... } +if (int.TryParse(s2, out var j)) { ... i ... } // copy/paste bug - still reading i instead of j +``` + +It works the same as when people had to declare their own variables outside the `if`, but it seems a bit of a shame that we can't do better now. Could we do something with definite assignment of out variables that could prevent this situation? + +Conclusion +---------- + +Whatever we could do here would be too specific for a language solution. It would be a great idea for a Roslyn analyzer, which can +- identify `TryFoo` methods by looking for "Try" in the name and the `bool` return type +- find this bug in existing code declaring the variable ahead of time, *as well as* in new code using out variables + + +Underbar wunderbar +================== + +Can we make `_` the wildcard instead of `*`? We'd need to be sneaky in order to preserve current semantics (`_` is a valid identifier) while allowing it as a wildcard in new kinds of code. + +``` c# +M(out var _); +int _ = e.M(_ => ..._...x...); // outer _ is still a named variable? +(int x, var _) = e.M(_ => ..._...x...); +(_, _) = ("x", 2); +``` +We would need to consider rules along the following lines: + +* Make `_` always a wildcard in deconstructions and patterns +* Make `_` always a wildcard in declaration expressions and patterns +* Allow `_` as a wildcard in other l-value situations (out arguments at least, but maybe assignment) when it's not already defined +* Allow `_` to be declared more than once in the same scope, in which case it is a wildcard when mentioned +* Allow `_` to be "redeclared" in an inner scope, and then it's a wildcard in the inner scope + +"Once a wildcard, always a wildcard!" + +Conclusion +---------- + +This is worth pursuing. More thinking is needed! + diff --git a/meetings/LDM-2016-11-15.md b/meetings/LDM-2016-11-15.md new file mode 100644 index 0000000000..87aff66ca4 --- /dev/null +++ b/meetings/LDM-2016-11-15.md @@ -0,0 +1,89 @@ +C# Language Design Notes, Nov 15, 2016 +====================================== + +> *Quote of the day*: "Bad people don't get good features!" + + +Agenda +------ + +- Tuple name warnings +- "Discards" + + +Tuple name warnings +=================== + +There are two kinds of warnings on the table regarding names in tuples: + +1. A warning on tuple *literals* only, if the literal uses an element name that isn't in the target type +2. A warning on all expressions of tuple type if an element name in the expression is used for a *different* element in the target type + +The first warning helps get the names right when writing a tuple literal, and while maintaining it. It is already in the product: + +``` c# +(int x, int y) t = (a: 1, b: 2); // Oops, forgot to update tuple literal? Names a and b would be lost +``` + +The second warning would help guard against bugs due to reordering of tuple elements: + +``` c# +(string firstName, string lastName) GetName(); + +(string lastName, string firstName) name = GetName(); // Oops, forgot to swap the element names in name? +``` + +This warning is currently *not* in the product. We would like it to be, but don't have the runway to implement it. + +In the future it could be added as an analyzer, or as a compiler warning protected by warning waves (#1580). + + +"Discards" +========== + +We got great feedback on "wildcards" at the MVP Summit, including changing its name to the term "discards", which is spreading in industry. + +We are also encouraged to use `_` as the discard character instead of `*`. It works better visually, and is what many other languages already use. + +There is a small matter of `_` already being a valid identifier. In order for it to coexist as a discard with existing valid uses we need to use a few tricks. Here are the essential rules: + +1. A standalone `_` when no `_` is defined in scope is a discard +2. A "designator" `var _` or `T _` in deconstruction, pattern matching and out vars is a discard + +Discards are like unassigned variables, and do not have a value. They can only occur in contexts where they are assigned to. + +Examples: + +``` c# +M(out _, out var _, out int _); // three out variable discards +(_, var _, int _) = GetCoordinates(); // deconstruction into discards +if (x is var _ && y is int _) { ... } // discards in patterns +``` + +One use case is to silence warnings when dropping Tasks from async methods: + +``` c# +_ = FooAsync(); +``` + +We also want to allow discards as lambda parameters `(_, _) => 0`, but don't expect that to make it in C# 7.0. Today `_` is allowed as an ordinary identifier; the rule would be that it would become a discard if there's more than one. + +We can also consider allowing top-level discards in foreach loops: + +``` c# +foreach (_ in e) { ... } // I don't care about the values +``` + +But that does not seem too important, especially since you can use `var _` to the same effect. + +In general, whenever use of standalone `_` as a discard is precluded, either by syntactic restrictions or by `_` being declared in the scope, `var _` is often a fine substitute with the same meaning. However, if the declared `_` is a parameter or local, there are situations where you are out of luck: + +``` c# +public override void M(int _) // declaring _ to signal that this override doesn't care about it +{ + _ = TryFoo(); // Error: cannot assign bool result to int variable _ + var _ = TryFoo(); // Error: cannot declare local _ when one is already in scope +} +``` + +These situations are always local to a member, so it is relatively easy to rewrite them, e.g. to rename the incoming parameter. We could consider accommodating them in the future: the error for a local redeclaration of `_` when one is already in scope could be changed to allowing it, but consider the second declaration a discard. This may be too subtle, and not useful enough, but it is worth considering post C# 7.0. \ No newline at end of file diff --git a/meetings/LDM-2016-11-30.md b/meetings/LDM-2016-11-30.md new file mode 100644 index 0000000000..7dc220653a --- /dev/null +++ b/meetings/LDM-2016-11-30.md @@ -0,0 +1,126 @@ +C# Language Design Notes for Nov 30, 2016 +========================================= + + +Agenda +------ + +- Scope of while condition expression variables +- Mixed deconstruction +- Unused expression variables +- Declarations in embedded statements +- Not-null pattern + + +Scope of while condition expression variables +============================================= + +We are compelled by the argument in #15529 about the scope of expression variables occurring in `while` conditions. + +``` c# +while (src.TryGetNext(out int x)) +{ + // Same or different `x` each time around? +} +// x in scope here? +``` + +More broadly we seem to have the following options for scopes and lifetimes of such declarations: + +1. There is a single variable `x` for the whole `while` statement, which is initialized repeatedly, each time around. It is in scope outside the `while` statement. +2. There is a single variable `x` for the whole `while` statement, which is initialized repeatedly, each time around. It is only in scope inside the `while` statement. +3. There is a fresh variable `x` for each iteration of the `while` statement, initialized in the condition. It is only in scope inside the `while` statement. + +There is a clear argument for the lifetime of the variable being a single iteration. We know from `foreach` that this leads to better capture in lambdas, but perhaps more importantly it avoids the odd behavior of the same variable getting initialized multiple times. (Pretty much the only way to otherwise achieve that in C# is through insidious use of `goto`). + +This puts us squarely in option 3 above, where it is meaningless to put "the" variable in scope outside of a while loop. Indeed, from the outside there is no notion of "the" variable: there will be multiple `x`es over the execution of the loop. + +Consequences in for loops +------------------------- + +In light of a changed `while` loop we also need to examine what that means to the `for` loop, which, while not defined as such in the spec, can be informally understood as "desugaring" into a `while` loop. + +``` c# + +for (; ; ) + +==> + +{ + + while() + { + + cont: + { } + } +} +``` + +So with respect to scopes and lifetimes, `` should behave the same as the condition in a while loop. + +Similarly, `` should be a fresh variable each time around. Furthermore it should occur in its own little nested scope, since variables introduced in it won't be definitely assigned until the end of each iteration. + + +Conclusion +---------- + +We want to change to the narrow scopes and short lifetime for expression variables introduced in the condition of while and for loops, and in the increment of for loops. + +This means that the `if` statement becomes more of a special case, with expression variables in its condition having broad scope. That is probably a good place to land. The main scenarios for this behavior are driven by the if statement to begin with, and it is also the one kind of statement where the lifetime and definite assignment situation for such variables makes it reasonable for them to persist outside of the statement. + + +Mixed deconstruction +==================== + +The reinterpretation of deconstruction in terms of declaration expressions would let us generalize so that: + +- Would allow mixing existing and newly declared variables in a deconstruction +- Would allow newly declared variables in deconstruction nested in expressions +- No error when occuring as an embedded statement + +Conclusion +---------- + +From a language design point of view we are confident in this generalization of the deconstruction feature. It is doubtful whether the change can make it into C# 7.0 at this point. Even though it is a small code change in and of itself, it introduces testing work and general churn. + + +Warn about unused expression variables? +======================================= + +Should we warn when an expression variable goes unused? Typically such variables would be dummy variables, and with discards `_` you no longer need them. + +On the other hand, we could leave it to the IDE to suggest discards etc, rather than give a warning from the language. + +Conclusion +---------- + +Let's not add the warning. Keep the warning in place where it already is in existing C#. + + +Declarations in embedded statements +=================================== + +C# currently has a grammatically enforced restriction against declaring "useless" variables as "embedded statements": + +``` c# +if (x > 3) var y = x; // Error: declaration statement not allowed as an embedded statement +``` + +Of course expression variables now give you new ways of similarly declaring variables that are immediately out of scope and hence "useless". Should we give errors for those new situations also? + +Conclusion +---------- + +Let's not add errors for deconstruction situations. In principle we probably want to remove even the existing limitation, but it's work we won't push for in C# 7.0. + + +Pattern to test not-null? +========================= + +It's been suggested to have a pattern for testing non-nullness (just as there is a `null` pattern for checking nullness). + +Conclusion +---------- + +No time in C# 7.0 but a good idea. diff --git a/meetings/LDM-2016-12-07,14.md b/meetings/LDM-2016-12-07,14.md new file mode 100644 index 0000000000..a7f416d4de --- /dev/null +++ b/meetings/LDM-2016-12-07,14.md @@ -0,0 +1,162 @@ +C# Language Design Notes for Dec 7 and Dec 14, 2016 +================================================= + +Agenda +------ + +- Expression variables in query expressions +- Irrefutable patterns and reachability +- Do-while loop scope + + +Expression variables in query expressions +========================================= + +It seems desirable to allow expression variables in query clauses to be available in subsequent clauses: + +``` c# +from s in strings +where int.TryParse(s, out int i) +select i; +``` + +The idea is that the `i` introduced in the `where` clause becomes a sort of extra range variable for the query, and can be used in the `select` clause. It would even be definitely assigned there, because the compiler is smart enough to figure out that variables that are "definitely assigned when true" in a where clause expression would always be definitely assigned in subsequent clauses. + +This is intriguing, but when you dig in it does raise a number of questions. + +Translation +----------- + +How would a query like that be translated into calls of existing query methods? In the example above we would need to split the `where` clause into a call to `Select` to compute both the boolean result and the expression variable `i`, then a call to `Where` to filter out those where the boolean result was false. For instance: + +``` c# +strings + .Select(s => new { s, __w = int.TryParse(s, out int i) ? new { __c = true, i } : new { __c = false, i = default } }) + .Where(__p => __p.__w.__c); + .Select(__p => __p.__c.i); +``` + +That first `Select` call is pretty unappetizing. We can do better, though, by using a trick: since we know that the failure case is about to be weeded out by the `Where` clause, why bother constructing an object for it? We can just null out the whole anonymous object to signify failure: + +``` c# +strings + .Select(s => int.TryParse(s, out int i) ? new { s, i } : null) + .Where(__p => __p != null) + .Select(__p => __p.i); +``` + +Much better! + +Other query clauses +------------------- + +We haven't really talked through how this would work for other kinds of query clauses. We'd have to go through them one by one and establish what the meaning is of expression variables in each expression in each kind of query clause. Can they all be propagated, and is it meaningful and reasonable to achieve? + +Mutability +---------- + +One thing to note is that range variables are immutable, while expression variables are mutable. We don't have the option of making expression variables mutable across a whole query, so we would need to make them immutable either: +- everywhere, or +- outside of the query clause that introduces them. + +Having them be mutable inside their own query clause would allow for certain coding patterns such as: + +``` +from o in objects +where o is int i || (o is string s && int.TryParse(s, out i)) +select i; +``` + +Here `i` is introduced and then mutated in the same query clause. + +The above translation approaches would accommodate this "mutable then immutable" semantics if we choose to adopt it + +Performance +----------- + +With a naive query translation scheme, this could lead to a lot of hidden allocations even when an expression variable is *not* used in a subsequent clause. Today's query translation already has the problem of indiscriminately carrying forward all range variables, regardless of whether they are ever needed again. This feature would exacerbate that issue. + +We could think in terms of language-mandated query optimizations, where the compiler is allowed to shed range variables once they are never referenced again, or at least if they are never referenced outside of their introducing clause. + + +Blocking off +------------ + +We won't have time to do this feature in C# 7.0. If we want to leave ourselves room to do it in the future, we need to make sure that we don't allow expression variables in query clauses to mean something *else* today, that would contradict such a future. + +The current semantics is that expression variables in query clauses are scoped to only the query clause. That means two subsequent query clauses can use the same name in expression variables, for instance. That is inconsistent with a future that allows those variables to share a scope across query clause boundaries. + +Thus, if we want to allow this in the future we have to put in some restrictions in C# 7.0 to protect the design space. We have a couple of options: + +- Disallow expression variables altogether in query clauses +- Require that all expression variables in a given query expression have different names + +The former is a big hammer, but the latter requires a lot of work to get right - and seems at risk for not blocking off everything well enough. + +Deconstruction +-------------- + +A related feature request is to allow deconstruction in the query clauses that introduce new range variables: + +``` c# +from (x, y) in points +let (dx, dy) = (x - x0, y - y0) +select Sqrt(dx * dx + dy * dy) +``` + +This, again, would simply introduce extra range variables into the query, and would sort of be equivalent to the tedious manual unpacking: + +``` c# +from __p1 in points +let x = __p1.Item1 +let y = __p1.Item2 +let __p2 = (x - x0, y - y0) +let dx = __p2.Item1 +let dy = __p2.Item2 +select Sqrt(dx * dx, dy * dy) +``` + +Except that we could do a much better job of translating the query into fewer calls: + +``` c# +points + .Select(__p1 => new { x = __p1.Item1, y = __p1.Item2 }) + .Select(__p2 => new { dx = __p2.x - x0, dy = __p2.y - y0, * = __p2 } + .Select(__p3 => Sqrt(__p3.dx * __p3.dx, __p3.dy * __p3.dy) +``` + +Conclusion +---------- + +We will neither do expression variables nor deconstruction in C# 7.0, but would like to do them in the future. In order to protect our ability to do this, we will completely disallow expression variables inside query clauses, even though this is quite a big hammer. + + +Irrefutable patterns and reachability +===================================== + +We could be smarter about reachability around irrefutable patterns: + +``` c# +int i = 3 +if (i is int j) {} +else { /* reachable? */ } +``` + +We could consider being smart, and realizing that the condition is always true, so the else clause is not reachable. + +By comparison, though, in current C# we don't try to reason about non-constant conditions: + +``` c# +if (false && ...) {} +else { /* reachable today */ } +``` +Conclusion +---------- + +This is not worth making special affordances for. Let's stick with current semantics, and not introduce a new concept for "not constant, but we know it's true". + + +Do-while loop scope +=================== + +In the previous meeting we decided that while loops should have narrow scope for expression variables introduced in their condition. We did not explicitly say that the same is the case for do-while, but it is. \ No newline at end of file diff --git a/meetings/LDM-2017-01-17.md b/meetings/LDM-2017-01-17.md new file mode 100644 index 0000000000..4c85072557 --- /dev/null +++ b/meetings/LDM-2017-01-17.md @@ -0,0 +1,50 @@ +# C# Language Design Notes for Jan 17, 2017 + +## Agenda + +A few C# 7.0 issues to review. + +1. Constant pattern semantics: which equality exactly? +2. Extension methods on tuples: should tuple conversions apply? + +# Constant pattern semantics + +Issue #16513 proposes a change to the semantics of constant patterns in `is` expressions. For the code + +``` c# +e is 42 +``` + +We currently generate the call `object.Equals(e, 42)` (or equivalent code), but we should instead generate `object.Equals(42, e)`. + +The implementation of `object.Equals` does a few reference equality and null checks, but otherwise delegates to the instance method `Equals` of its *first* argument. So with the current semantics the above would call `e.Equals(42)`, whereas in the proposal we would call `42.Equals(e)`. + +The issue lists several good reasons, and we can add more to the list: + +- The constant pattern isn't very *constant*, when it's behavior is determined by the non-constant operand! +- Optimization opportunities are few when we cannot depend on known behavior of calling `c.Equals` on a constant value. +- Intuitively, the pattern should do the testing, not the object being tested +- Calling a method on the expression could cause side effects! +- The difference from switch semantics is jarring +- Switching would preserve the nice property of `is` expressions today that it only returns `true` if the left operand is implicitly convertible to the (type of the) right. + +There really is no downside to this, other than the little bit of work it requires to implement it. + +## Conclusion + +Do it. + + +# Extension methods on tuples + +Issue #16159 laments the facts that extension methods only apply to tuples if the tuple types match exactly. This is because extension methods currently only apply if there is an *identity, reference or boxing conversion* from the receiver to the type of the extension method's first parameter. + +The spirit of this rule is that if it applies to a type or its bases or interfaces, it will work. We agree that it *feels* like it should also work for tuples - at least "sometimes". We cannot make it just always work for tuple conversions, though, since they may recursively apply all kinds of conversions, including user defined conversions. + +We could check *recursively* through the tuple type for "the right kind of conversion". Compiler-wise this is a localized and low-risk change. It makes tuples compose well with extension methods. It's another place where things should "distribute over the elements" of the tuple. + +This is a now-or-never kind of change. It would be a breaking change to add later. + +## Conclusion + +Try to do it now if at all possible.