Skip to content

Latest commit

 

History

History
79 lines (61 loc) · 6.85 KB

LDM-2024-03-11.md

File metadata and controls

79 lines (61 loc) · 6.85 KB

C# Language Design Meeting for March 11th, 2024

Agenda

Quote of the Day

  • Someone in the room picks up a knife to slice some bread "That is going to make it impossible for the people online to hear" "Thank you for advocating for us" "Thank you for also drawing attention to the fact that we don't have any bread"

Discussion

Dictionary expressions

#7822
https://github.com/dotnet/csharplang/blob/dcf46ba377e1ce356f69f981f36b2273c3179ca3/proposals/dictionary-expressions.md

Today, we took a look at the first proposal for dictionary expressions. There are a number of open questions that need to be answered, including syntax decisions; however, in order to make progress on the more thorny semantics questions, we are using the [key: value] strawman syntax from the proposal for now. We will make concrete decisions on the syntax at a later time. The syntax we end up with is important, of course, but fairly unconnected to the semantics we need to delve into at the moment.

The first two questions we looked at are highly related. They are:

  1. Should we allow KeyValuePair expression elements in dictionary expressions?
  2. Should we allow spreading an enumerable of KeyValuePair in a dictionary expression?

The first question can even be expressed in terms of the second: after all, if we allow the second, and we give collection expressions a natural type, then users could just do [.. [keyValuePairExpression]] even if we don't allow 1. This question brought up a recurring theme through the rest of the open questions we looked at: we're currently calling these dictionary expressions, because that's a catchy name that immediately conveys the general target of these. However, what are they actually? Are they dictionary expressions, or are they actually collections of associated keys and values? The LDM is unanimously in favor of supporting both these scenarios.

Next, we looked at whether these expressions should be able to initialize generalized collection types, or just "dictionary" types. Specifically, can these types initialize a List<KeyValuePair<TKey, TValue>> and things like it, or are they stuck initializing things like Dictionary<TKey, TValue>. This is a trickier question; our correspondence principle for creation and consumption does not generally hold up for such scenarios. As an example:

List<KeyValuePair<string, int>> list = ["mads": 21];
Console.Write(list["mads"]); // Can't consume like this because it's a List

No matter what syntax we choose for the initialization form, there won't be a correspondence with usage here, as List<> is accessed by index, not by key. This gives a few members of the LDM pause, but overall, we're in favor of allowing this.

The next topic was on how restrictive we should be on KeyValuePair itself; namely, must the type involved be exactly System.Collections.Generic.KeyValuePair<TKey, TValue>, or should it be expanded? One example is (TKey string, TValue value). This tuple type is conceptually a KVP, and some collections use it or other similar tuple types instead of KVP. For example, PriorityQueue<TElement, TPriority> exposes an UnorderedItems property that is an IReadOnlyList<(TElement Element, TPriority Priority)>. We may want to enable merging of 2 queues by doing PriorityQueue<TElement, TPriority> p = [.. originalQueue1.UnorderedItems, .. originalQueue2.UnorderedItems];. This example raises several questions of its own (what APIs would we use, since there's neither Add nor indexers on PriorityQueue?), but there is at least some merit in considering the idea.
There's another question here, too: what about conversions between keys and values in the source collection expression and the destination? For example: Dictionary<int, int> d = [shortKey: shortValue];, where the key and value need to be subjected to a short->int conversion. Are conversions like this acceptable? This plays back into the question of what these collections actually are: if they're conceptually collection expressions, then the conversions around the keys and values may be less acceptable. However, if they're collections of associated keys and values, then subjecting the individual parts may be fine.
Ultimately, LDM did not reach a decision on this question today. We'll need to dig more into it in future meetings.

Finally, we looked at the semantics of how a dictionary expression will build its resulting value: does it overwrite existing values, or does it error when there are duplicate keys? There are few different interesting points here: we're currently calling these dictionary expressions. That, combined with how collection expressions are specified around Add methods, may give users a natural intuition that we'll be calling Add on the dictionary, which will throw when a duplicate is encountered. However, System.Collection.Generics.Dictionary<,> may not be the only dictionary a user would key off of, and .NET has very inconsistent behavior for what Add on a dictionary-like type will do. ImmutableDictionary<,> will not throw, and will keep the original value if the new value compares equal to the original value. FrozenDictionary<,> immediately throws NotSupportedExceptions; the list goes on, but the only consistent bit is that we're inconsistent in behavior. These discrepancies had us reconsider the question from another direction: rather than "What is the behavior", we instead want to consider "What APIs are we calling"; this will allow each scenario to tailor its APIs to handle the question directly, rather than forcing a single behavior on all collection types.
There's some concern among members that using an indexer, rather than calling an Add method, will lead to concerning behavioral differences if a user decides to refactor existing collection initializers to dictionary expressions. We do have existing behavior in our IDE fixers that can help convey that a refactoring is not exactly semantics-preserving, but it would require the user understanding the exact difference and examining usages to ensure that they're not falling afoul of bad behavior in the presence of indexer assignment. However, a larger portion of the LDM is more concerned that, in the presence of spreads (which have no collection initializer analogy), Add is more often the wrong thing to call than using an object initializer. That is the direction we'll go with for now, and explore how we specify the rules given indexer usage instead of Add calls.

Conclusions

Questions 1 and 2 are wholeheartedly approved; KVP expressions and spreads of KVP collections will work in dictionary expressions.
Question 3 is approved; dictionary expressions will be able to convert to collection types that have a KVP element type.
Question 4 needs more time to be considered.
Question 5 settled on using indexers as the lowering form.