csharplang/meetings/2016/LDM-2016-05-03-04.md
2017-01-31 11:30:19 -08:00

8.3 KiB

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 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:

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.

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:

(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:

Dictionary<int, string> d = (16, EqualityComparer<int>.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:

Point p = new (3, 4); // Same as new Point(3, 4)
List<string> l1 = new (10); // Works for 0 or 1 argument
List<int> 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<...>.