Skip to content

Latest commit

 

History

History
81 lines (56 loc) · 4.69 KB

LDM-2023-06-19.md

File metadata and controls

81 lines (56 loc) · 4.69 KB

C# Language Design Meeting for June 19th, 2023

Agenda

Quote(s) of the Day

  • "What's the second question?" "Type inference for collection literals." "Oh that's easy"
  • "Second2, also known as third"
  • "Oh, I thought [redacted] was making a 'type inference walks into a bar' joke"

Discussion

Prefer spans over interfaces in overload resolution

#7276

Today we looked at whether overload resolution should have special tie-breaking rules for the Span<T> vs IEnumerable<T> case. There are a few cases that can this affect, and potentially blocks the runtime from adding overloads that take ReadOnlySpan<T> for fear of breaking users.

var ia1 = ImmutableArray.CreateRange<int>(new[] { 1, 2, 3 }); // error: CreateRange() is ambiguous
var ia2 = ImmutableArray.CreateRange<char>("Test"); // error: CreateRange() is ambiguous

public static class ImmutableArray
{
    public static ImmutableArray<T> CreateRange<T>(IEnumerable<T> items) { ... }
    public static ImmutableArray<T> CreateRange<T>(ReadOnlySpan<T> items) { ... }
}

For both examples, these are types that are convertible to both a Span<T> and to IEnumerable<T>. There is no commonality between those two types, other than object, so the overloads are ambiguous. There are two questions here:

  • Do we want to mimic a subtype relationship between IEnumerable<T> and Span<T> as best we can without Span<T> actually implementing the interface?
  • Is Span<T> special here? Are there other user-defined ref structs that can run into this same problem with ambiguous overloads?

We're a bit concerned with chasing a subtype relationship here. Ideologically, Span<T> is indeed an IEnumerable<T>, but since ref structs cannot implement interfaces today, the relationship isn't there. But that relationship plays in more than just overload resolution. Generics, for example, which is another place ref structs cannot be used today. It feels like trying to mimic the behavior as best as possible would be a large rabbit hole to go down, when we do want ref structs to be able to implement interfaces at some point. Implementing that would solve this issue and in a more general fashion.

We also talked a bit about workaround the runtime could do for these cases. Defining more overloads is an obvious answer for some scenarios, such as the ImmutableArray case above. That could define CreateRange<T>(T[] array) and CreateRange(string s); indeed, this is the solution that DefaultInterpolatedStringHandler used to avoid ambiguities in AppendFormatted. But that doesn't work in every case. For example, constructors of generic types cannot have specialized versions for a specific generic. In other words, HashSet<char> (and only HashSet<char>) cannot have a constructor that takes a string to resolve that ambiguity. Extensions might be able to solve that, but it doesn't seem like it would give the actual end experience we want, namely the subtype relationship between Span<T> and IEnumerable<T>. Given this, we plan on waiting until ref structs can implement interfaces and then all of these issues will work themselves out with no additional rules needed.

Conclusion

No language change here.

Collection literals

#5354
#7284

We also looked at the proposed type inference for collection literals. We explored our existing behavior here around several areas:

  • Collection initializers on arrays - Arrays are not target-typed today, so conversion-from-expressions do not carry through the collection initializer here. We think this actually makes array collection initializers a poor analogy for where to draw behavior from.
  • Switch expressions - These are target typed, and arm types can influence the type inference of a method.
  • Tuple literals - These might be the best analogy. They are target typed, and have a general desired behavior of "you should be able to replace the tuple with the elements and have the same behavior". In other words, for a signature of M<T>((T, T) tuple), passing a tuple literal should have the same outcome as passing each element individually to a signature of M<T>(T t1, T t2).

We like the tuple analogy for collection literals. The behavior we want is the equivalent, spelled out as:

If there is a signature M<T>(T[] array), passing a collection literal of [1, 2] should have the same effect as if the signature was M<T>(T t1, T t2).

Conclusion

Treat collection literals transparently for type inference.