csharplang/meetings/2021/LDM-2021-02-03.md
2021-02-03 15:27:11 -08:00

6 KiB

C# Language Design Meeting for Feb 3rd, 2021

Agenda

  1. List patterns on IEnumerable
  2. Global usings

Quote of the Day

  • "If the teacher doesn't show up, class is dismissed, right?" "Who is the teacher in this scenario?"

Discussion

List patterns on IEnumerable

https://github.com/alrz/csharplang/blob/list-patterns/proposals/list-patterns.md

Today, we discussed what the behavior of list patterns should be for IEnumerable, and specifically how much of list patterns should be able to operate on enumerables. There are multiple competing factors here that we need to take into consideration.

  1. IEnumerables can be infinite. If a user were to attempt to match the count of such an enumerable, their code hangs.
  2. IEnumerables can be expensive. Likely more common than the infinite case, enumerable can be a DB query that needs to run off to some data server in the cloud when queried. We absolutely do not want to enumerate these multiple times.
  3. We do not want to introduce a new pitfall of "Oh, you're in a hot loop, remove that pattern match because it'll be slower than checking the pattern by hand".

All of that said, we do think that list patterns on enumerables are useful. While this can be domain specific, efficient enumeration of enumerables is relatively boilerplate code and with some smart dependence on framework APIs, we think there is a path forward. For example, the runtime just approved a new API for TryGetNonEnumeratedCount, and in order to make the pattern fast we could attempt to use it, then fall back to a state-machine-based approach if the collection must be iterated. This would give us the best of both worlds: If the enumerable is actually backed by a concrete list type, we don't need to do any enumeration of the enumerable to check the length pattern. If it's not, we can fall back to the state machine, which can do a more efficient enumeration while checking subpatterns than we could expose as an API from the BCL.

For the state machine fallback, we want to be as efficient as possible. This means not enumerating twice, and bailing out as soon as possible. So, the pattern enumerable [< 6] { 1, 2, 3, .., 10 } can immediately return false if it gets to more than 6 elements, or if any of the first 3 elements don't match the supplied patterns.

Finally, on the topic of potentially infinite or expensive enumerations, they are an existing problem today. The BCL exposes a Count API, and if you call it on a Fibonacci sequence generator, your program will hang. Enumerating db calls is expensive, regardless of whether we provide a new, more succinct form or not. In these cases, users generally know what they're working with: it's not a surprise that they have an infinite enumerable, they've very likely already done a Take or some other subsequence mechanism if they're looking for "the last element from the end". By having these patterns, we simply allow these users to take advantage of a generation strategy that's as efficient as they could write by hand, with much clearer intent. As long as the enumeration has a specific pattern that users can reason about, it's an overall win.

Conclusion

We'll proceed with making a detailed specification on how IEnumerable will be pattern matched against. We're ok with taking advantage of BCL APIs here, including TryGetNonEnumeratedCount, and are comfortable working with the BCL team to add new APIs if existing ones don't prove complete enough for our purposes.

Global usings

We started this by looking at a prototype of how ASP.NET is looking to reduce ceremony in their templates with a framework called Feather, which can be seen here. The hello world for this code is 12 lines long: 6 lines of actual code, 3 newlines, and 3 lines of usings. As apps get more complicated, these usings tend to grow quite quickly, and they're all for the types of things that often boil down to "I want to use the async feature from C# 5, LINQ from C# 3, generic collections from C# 2, and I want to build an ASP.NET application". This hints at a related, but orthogonal, using feature: recursive usings. For example, using System.* would bring in all namespaces under System, or using Microsoft.AspNetCore.* would bring in all namespaces under Microsoft.AspNetCore. However, such a feature wouldn't really solve the issue in question here, which is "how can specifying the SDK in use ensure that I get the ability to use the features of that SDK by default?"

We have 2 general approaches here: use the project file as the place where implicit usings go, or allow a source file to include them. Both approaches have several pros and cons. In a project file works more natively for an SDK, as they can just define a property. The SDK does define an AssemblyVersion.cs today, but this feature is potentially more complicated than that. The project file is also where we tend to put these types of global controls, like nullable or checked. On the other hand, project files are very hard to tool, as MSBuild is a complicated beast that can do arbitrary things. Artificial restrictions on the feature, like requiring that it appear directly in the project file and not in some other targets file, severely limits the usefulness of the feature across solutions. Source files as the solution provide an easily-toolable experience that feels more C#-native, but potentially encourages these usings to be spread out in many locations. Razor has a _ViewImports.cshtml file that handles this problem for Razor files, but we don't think this maps well to the solutions we're discussing for C#: it only allows the one file, and is in some ways the "project file" for the rest of the cshtml files in the solution as it provides things like the namespace of the rest of the pages.

Conclusion

We're split right down the middle here between project file and C# files. We'll revisit this again very shortly to try and make progress on the feature.