Roslyn: Pattern-matching versus arrays, dictionary, list, dynamic?

Created on 16 Apr 2016  路  7Comments  路  Source: dotnet/roslyn

  • [ ] Will we support pattern matching with arrays?
  • [ ] Will we support pattern matching with List, Dictionary? Think particularly of F#'s really useful list deconstruction, which feels impossible with IEnumerable since patterns shouldn't generally be executing methods like GetEnumerator/MoveNext.
  • [ ] How should the type dynamic interact with pattern matching?
Area-Language Design Language-C# New Language Feature - Pattern Matching

Most helpful comment

Is there an issue using array/collection initializer syntax for list patterns that would apply to anything IList<T>, including arrays?

int[] array = new [] { 1, 2, 3 };
var list = new List<int>() { 1, 2, 3 };

let { 1, var second, 2 } = array else return;
let { 1, 2, var third } = list else return;

A sequence wildcard pattern would be nice to indicate that you don't care how many previous or subsequent elements there might be:

var list = GetListOfNumbers();
let { var first, **, var last } = list else return;

F# has no such facility.

The cons pattern is interesting, but I'm not aware of a CLR type that it would map to cleanly. IIRC in functional languages it always is used to construct/deconstruct an immutable linked list. IEnumerable<T> is academically interesting but probably impractical.

As for dynamic, I don't think that the current implementation provides a way to inspect truly dynamic types? Otherwise, I'd try to support the gamut of patterns through emitting code that inspects the dynamic type for the appropriate shape.

dynamic expando = new ExpandoObject();
expando.Foo = 123;
expando.Bar = "456";

switch (expando) {
    case { Foo is 123, Bar is "456" }:
        Console.WriteLine("matched!");
        break;
}

All 7 comments

Is there an issue using array/collection initializer syntax for list patterns that would apply to anything IList<T>, including arrays?

int[] array = new [] { 1, 2, 3 };
var list = new List<int>() { 1, 2, 3 };

let { 1, var second, 2 } = array else return;
let { 1, 2, var third } = list else return;

A sequence wildcard pattern would be nice to indicate that you don't care how many previous or subsequent elements there might be:

var list = GetListOfNumbers();
let { var first, **, var last } = list else return;

F# has no such facility.

The cons pattern is interesting, but I'm not aware of a CLR type that it would map to cleanly. IIRC in functional languages it always is used to construct/deconstruct an immutable linked list. IEnumerable<T> is academically interesting but probably impractical.

As for dynamic, I don't think that the current implementation provides a way to inspect truly dynamic types? Otherwise, I'd try to support the gamut of patterns through emitting code that inspects the dynamic type for the appropriate shape.

dynamic expando = new ExpandoObject();
expando.Foo = 123;
expando.Bar = "456";

switch (expando) {
    case { Foo is 123, Bar is "456" }:
        Console.WriteLine("matched!");
        break;
}

As I have said in one of the other issues, I feel that arrays/lists will be well-served by a cons pattern coupled with list views/slices/segments. Enumerables are better off with LINQ. Dynamics can be matched against a property pattern. I have nothing to offer wrt dictionaries.

I think complete analogy with _object-or-collection-initializer_ would be really neat and I don't think it would be ambiguous at all (that's why we have a _non-assignment-expression_ in collection initializers).

And for dictionaries we can use indexer patterns (#10600 (comment)).

Is there an issue using array/collection initializer syntax for list patterns that would apply to anything IList, including arrays?

If possible, please make that some sort of duck-typed IReadOnlyList<T> instead - IEnumerable<T> with an indexer with a getter of T and a long or int Count. (In the tradition of awaitables, foreach and collection initializers being speced using duck typing instead of interfaces. If IReadOnlyList<T> had been there right in 2.0, everyone would have adopted it and this wouldn't have been necessary.)

A sequence wildcard pattern would be nice to indicate that you don't care how many previous or subsequent elements there might be:

I propose using var first, ..., var last instead, which conveys more "multi-elemental-ness" (it is literally the yada-yada-yada operator in Perl) than just repeating the *. Or why not var first, params, var last? (Just kidding. 馃槣)

For dictionaries, how about:

var d = new Dictionary<string, int> {
    ["trite"] = 42
};
switch (d) {
    case var answer = ["trite"]:
        // ...
}

var elaborate = new Dictionary<string, string {
    ["type"] = "string",
    ["integer"] = null,
    ["string"] = "xyz"
};
switch (elaborate) {
    // I am unsure about whether the following syntax is enabled by recursive patterns
    case var value = [var type = ["type"]]:
        // ...
}

To avoid exceptions when getting things that are simply not there, this would use, in order of availability:

  • TryGetValue(TKey, out TValue) (implementable without being a dictionary, again like Add in collection initializers, would be picked up in IReadOnlyDictionary<TKey, TValue> and Dictionary<TKey, TValue>)
  • Maybe allowable: Contains(TKey) followed by this[TKey] => TValue; not atomic and could throw during races, but not more or less dangerous than just writing that code outside of a pattern. Limited to dictionaries (like non-generic IDictionary and Hashtable) that don't implement TryGetValue.
  • Very probably not allowable: just this[key], but throw if the indexer did

The above pattern could also be adapted to random access list/collection index matching.

Pattern matching with arrays & lists will be a bit difficult since C# (still) doesn't have array & list literals!

Normally pattern matching is understandable because the syntax to deconstruct and to construct are the same. Consider tuples: var (x, y) = (1, 2);. There is a pleasing, easy-to-understand symmetry to this form, thanks to the tuple literal. How would it work with arrays?

let (new [] { var first }) = new [] {1, 2, 3} else return;

This has symmetry but looks really awkward... Any chance of C# finally having real, honest to god list and array literals? I'm going to completely rip off F# here with a suggestion:

let myList = [1, 2, 3];

switch (myList)
{
    case [var first, **] : return first;
    case [] : throw new InvalidOperationException("Cannot get first item of empty list");
}

And for arrays:

let myArray = [| 'a', 'b', 'c' |];

switch (myArray)
{
    case [| var first, ** |] : return first;
    case [| |] : throw new InvalidOperationException("Cannot get first item of empty array");
}

While the cons idea seems right at first glance, I don't think it's right for C# since List is not a singly-linked list that can easily be pushed and popped like a stack.

IEnumerable is not appropriate for deconstructing because, as hinted at above, you're quite possibly causing side effects or causing re-evaluation of some sequence by walking the IEnumerable.

Actually, a better idea might be to support some duck typing here.

The case [var first, var second] pattern can be used to deconstruct _any type_ that supports an int-indexer, i.e. implements this[int index]. Perhaps the type should also implement Count since case [var first, var second] technically will only match a list of length 2;

Issue moved to dotnet/csharplang #898 via ZenHub

Was this page helpful?
0 / 5 - 0 ratings