New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion - Deconstruction of Collections/Enumerables #874

Open
jcouv opened this Issue Sep 2, 2017 · 13 comments

Comments

Projects
None yet
10 participants
@jcouv
Member

jcouv commented Sep 2, 2017

@Velocirobtor commented on Sat Oct 01 2016

Since Deconstruction is probably going to make it into C#, I think it would be awesome to allow Deconstruction of Collections/Enumerables:

var arr = new[]{1, 2, 3, 4};

var (a, b, c, d) = arr;
var (x, y) = arr.Take(2);
// or if Slices make it into C# (Which would be awesome!)
var (v, w) = arr[1:-1];

Python for example already has a similar feature and I think it's quite useful.
As for a possible Implementation:
Since Deconstruction will probably be via a Deconstruction Method with out parameters, allowing out params could do the trick:

public static void Deconstruct<T>(this IEnumerable<T> collection, out params T[] values) {
    var enumerator = collection.GetEnumerator();
    for (int i = 0; i < values.Length; ++i) {
        if(!enumerator.MoveNext()) throw new DeconstructionException("Not enough values in collection");
        values[i] = enumerator.Current;
    }
    //and optional
    if(enumerator.MoveNext()) throw new DeconstructionException("Too many values in collection");
}

This way all Enumerables could be deconstructed.


@svick commented on Sat Oct 01 2016

Related: dotnet/roslyn#10631


@jcouv commented on Fri Sep 01 2017

Discussion of language features are being moved over to csharplang. I'll move this issue over.

@kevingosse

This comment has been minimized.

kevingosse commented Sep 3, 2017

The first two suggestions don't require changes at the language level (assuming an overload for each number of parameters is provided):

public static class EnumerableExtensions
{
    public static void Deconstruct<T>(this IEnumerable<T> values, out T item1, out T item2, out T item3)
    {
        var enumerator = values.GetEnumerator();

        enumerator.MoveNext();
        item1 = enumerator.Current;

        enumerator.MoveNext();
        item2 = enumerator.Current;

        enumerator.MoveNext();
        item3 = enumerator.Current;
    }

    public static void Deconstruct<T>(this IReadOnlyList<T> values, out T item1, out T item2, out T item3)
    {
        item1 = values[0];
        item2 = values[1];
        item3 = values[2];
    }
}

So maybe this should be an API proposal in CoreFX instead.

@HaloFour

This comment has been minimized.

Contributor

HaloFour commented Sep 3, 2017

@kevingosse

Needs an overload for every possible arity and can't be conditional, so an array of two elements could still be deconstructed into three variables. Conditional deconstruction solves that problem and requires language changes already so, in my opinion, it's worth evaluating whether or not a different syntax for collection deconstruction makes sense.

@yaakov-h

This comment has been minimized.

Contributor

yaakov-h commented Sep 3, 2017

@HaloFour Conditional deconstruction?

@alrz

This comment has been minimized.

Contributor

alrz commented Sep 3, 2017

While this is what a list pattern would do, you could already deconstruct a list with head/tail format,

var (head, tail) = list;
var (first, (second, rest)) = list;

You just need to populate the second out param with the tail. however, this works best only with a specific data structure

@kevingosse

This comment has been minimized.

kevingosse commented Sep 3, 2017

Needs an overload for every possible arity

I don't see that as an issue. There's already an implementation of ValueTuple for each arity.

can't be conditional, so an array of two elements could still be deconstructed into three variables

I don't understand, could you show an example?

@HaloFour

This comment has been minimized.

Contributor

HaloFour commented Sep 3, 2017

@kevingosse

Currently a Deconstruct method must return void and must assign all of the out arguments. So, given:

public static class ArrayExtensions {
    public static void Deconstruct<T>(this T[] input, out T item1, out T item2, out T item3) { ... }
}

When used like this:

var array = new int[] { 1, 2 };
var (a, b, c) = array;

The implementation is required to either successfully deconstruct into three variables, assigning some arbirary sentinal default value for c, or it must throw an exception. Neither is ideal.

As a part of the larger pattern matching feature is conditional deconstruction where Deconstruct may return bool to indicate success. Then the deconstruction can be used in a pattern matching expresxion something like this:

if (array is var (a, b, c)) {
    // array has 3 elements
}
@kevingosse

This comment has been minimized.

kevingosse commented Sep 3, 2017

@HaloFour

Thanks for the explanation, it's clear now. I personally think that throwing an exception would be ideal. If I used deconstruction on an array, it would mean that I have strong assumptions on the contents of the array, and I would expect that validation was properly done before reaching that point in the code. For instance, if that was parameters from a command:

if (args.Length < 2)
{
    // Incorrect parameters
}

var (path, mode) = args; // if the length is lower than 2, then an exception should be thrown, as something is definitely wrong with my validation code

That said, I understand it's a matter of habits and taste.

The conditional deconstruction is very interesting, as it allows to check the length implicitly.

@DavidArno

This comment has been minimized.

DavidArno commented Sep 4, 2017

As @alrz implies, there is already a well established pattern for handling deconstructing enumerations: cons. And C# already has suitable syntax for performing that head/tail split.

var arr = new[]{1, 2, 3, 4};
var (a, (b, (c, d))) = arr;

Regarding needing a specific data structure, the solution for me was to simply use a ConsEnumerable<T> type that caches the enumeration, using a linked list, as it is deconstructed. Thus avoiding having to eg create a new list for the tail.

@MgSam

This comment has been minimized.

MgSam commented Sep 7, 2017

I'm against this feature because it promotes a dangerous and bad design pattern- trying to store different pieces of data using number indexes in an array. This makes sense in a low level language but makes zero sense in C#. Use a real type (or a tuple!) to store your data instead.

@ufcpp

This comment has been minimized.

ufcpp commented Sep 7, 2017

using System;

static class ArrayDeconstruction
{
    public static void Deconstruct<T>(this T[] x, out T head, out Span<T> tail)
    {
        head = x[0];
        tail = new Span<T>(x).Slice(1);
    }

    public static void Deconstruct<T>(this Span<T> x, out T head, out Span<T> tail)
    {
        head = x[0];
        tail = x.Slice(1);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var x = new[] { 1, 2, 3, 4, 5 };
        var (head, tail) = x;
        Console.WriteLine(head);
        Console.WriteLine(tail.Length);
    }
}
@DavidArno

This comment has been minimized.

DavidArno commented Sep 7, 2017

@MgSam,

I'm against this feature because it promotes a dangerous and bad design pattern- trying to store different pieces of data using number indexes in an array

This claim makes no sense to me. You already can "store different pieces of data using number indexes in an array"; use an object array. I really can't think of a use case where being able to deconstruct that array to a tuple would then promote that behaviour as it would require the writing of custom deconstructors to handle those mixed types.

@alrz

This comment has been minimized.

Contributor

alrz commented Sep 7, 2017

trying to store different pieces of data using number indexes in an array

When you think of this as a pattern (that can fail) you can do the matching in a self-contained way.

For example, imagine you want to write an analyzer to convert as/null-check to use pattern matching, for that you should find a subsequent variable declaration and if statement.

var statements = block.Statements;
if (statements is { LocalDeclarationStatement localDecalration, IfStatement ifStatement, ___ })

(the last pattern is a sequence discard: #305)

We already do this kind of things manually but with recursive and extension patterns it becomes a lot more declarative. The final pattern-matching proposal is not out yet but I think we can extend deconstruction assignment to support irrefutable patterns: dotnet/roslyn#16183.

@gulshan

This comment has been minimized.

gulshan commented Sep 9, 2017

It seems related with the spread/rest syntax now Javascript have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment