Skip to content

Covariant Immutable Collections #28911

@gregsn

Description

@gregsn

related: https://github.com/dotnet/corefx/issues/5164

having dedicated covariant interfaces for the different immutable collection types (and implementing them on the immutable collection classes) would allow to add extension methods that allow to add elements to an immutable covariant collection.

The extension methods could check if the incoming IImmutableCovariantList<out T> is already a ImmutableList<T> - which should be true most of the time - and in this case use all the specific methods, like insert, add, remove of that class. Otherwise, we'd need to create a new ImmutableList<T> and then operate on that new instance.


Why all the trouble?

  • Because explaining that an immutable collection of bananas cannot be perceived as an immutable collection of fruits is a hard task and in fact, could from a user perspective be seen as a design flaw - not as bad as the standard mutable array being covariant, but still not that beautiful.
  • With the proposed design the extension methods for modification would only show up on that type, not on IReadonlyList<T>. related: Make IImmutableList covariant like IReadOnlyList, IEnumerable etc. #16011. You'd have an immutable type, that when adding or removing an element would give you a new snapshot of exactly the same type, where this type is covariant.

it looks like the idea works and doesn't seem to break anything

Basically it is just

  • an empty interface IImmutableCovariantList<T> implemented by Immutablelist<T>
  • extension methods for that interface (for now in a new namespace System.Collections.Immutable.Covariance)

Down there in the tests you also can see that how c# treats an ImmutableList<T> as an IImmutableCovariantList<T> to make adding an apple to a list of bananas work out. As a user you currently need to add the namespace System.Collections.Immutable.Covariance that I introduced to avoid blowing up the list of extension methods that you see in the intellisense list when working with a regular ImmutableList<T>.


            var firstBanana = new Banana();
            var bananas = ImmutableList<Banana>.Empty;

            bananas = bananas.Add(firstBanana);

            // add an apple to the banana(s) and you get fruits
            var fruits = bananas.Add<IFruit>(new Apple()); 
            Assert.Equal(fruits.Count, 2);

            // add a banana and you still have bananas
            bananas = bananas.Add(new Banana());
            Assert.Equal(bananas.Count, 2);

            // bananas can be seen as fruits
            fruits = bananas;

            var anotherApple = new Apple();

            // again messing with bananas that get confronted with apples all of a sudden
            IEnumerable<IFruit> applefollowedByBanana = bananas.SetItem<IFruit>(0, anotherApple);
            IEnumerable<IFruit> applefollowedByBanana2 = bananas.Replace(firstBanana, anotherApple, EqualityComparer<IFruit>.Default);

            Assert.Equal(applefollowedByBanana, applefollowedByBanana2);

here is the issue about covariance for classes:
dotnet/roslyn#171
it will not happen.

even though immutable collections would highly benefit: Eric Lippert implementing a class Stack<out T>: https://stackoverflow.com/questions/2733346/why-isnt-there-generic-variance-for-classes-in-c-sharp-4-0/2734070#2734070 ...

anyways, because of this unavailable CLR feature, the only way of introducing covariant types is via interfaces. so yes, the proposed solution is a hack. it hacks around the limitations of the underlying system by introducing an interface for each immutable collection class. An ImmutableDictionary<TKey, TValue> e.g. would implement IImmutableCovariantDictionary<TKey, out TValue>. The interface is there to ship the covariance feature, which is not shippable without that "hack". So class and covariant interface would always come in pairs.

A user just needs to include System.Collections.Immutable.Covariance in her/his using statements and she/he is now able to work with IImmutableCovariantDictionary<TKey, out TValue> whenever the need arises. Therefore she/he can avoid more complicated code, which would do the same.


In the implementation of the extension methods regarding IImmutableCovariantArray<out T> would internally use the CastUp idea. The API surface would not care about when optimizations like the CastUp idea are possible or not. It would work the same way for all the immutable collections, which would be beautiful.


If in 10 years classes in .Net suddenly can have covariant type parameters, the interfaces would get obsolete and many extension methods would magically run faster as list as ImmutableList<T> would typically be true (even if the incoming list is actually of type ImmutableList<TDerived>).

        public static IImmutableCovariantList<T> Insert<T>(this IImmutableCovariantList<T> list, int index, T element)
            => (list as ImmutableList<T> ?? list.ToImmutableList()).Insert(index, element);


if you look at the implementation it gets obvious that we could also return the classes on all extension methods.
e.g.

    bananas.SetItem<IFruit>(0, anotherApple);

would act on ImmutableList<Banana> (seen as IImmutableCovariantList<IFruit>) and return ImmutableList<IFruit>. The user would not even need to deal with the interface.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-System.Collectionsbacklog-cleanup-candidateAn inactive issue that has been marked for automated closure.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions