Skip to content
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

Add Cast<T> and CastFrom<TDerived> to ImmutableArray<T> #13980

Closed
jaredpar opened this issue Jan 12, 2015 · 31 comments
Closed

Add Cast<T> and CastFrom<TDerived> to ImmutableArray<T> #13980

jaredpar opened this issue Jan 12, 2015 · 31 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented api-needs-work API needs work before it is approved, it is NOT ready for implementation
Milestone

Comments

@jaredpar
Copy link
Member

The ImmutableArray<T> type should have easier to find and use APIs for converting between related array element types.

Rationale and Usage

The ImmutableArray<T> type is meant to be as close as possible to T[] in functionality without the possibility of mutation. One area where it is lacking or at least harder to discover is type conversions.

With arrays it is possible to convert T[] to U[] when T inherits from U. This conversion is implicit and requires no dynamic type checking.

Dog[] dogArray = new Dog[] { } ;
Animal[] animalArray = dogArray;

The equivalent code for ImmutableArray<T> is less intuitive:

ImmutableArray<Dog> dogArray = ImmutableArray.Create<Dog>();
ImmutableArray<Animal> animalArray = ImmutableArray.Create<Animal, Dog>(dogArray); 

There are a few issues with this approach:

  • The API requires two type parameters which must be manually specified. There is no possibility type inference will ever successfully infer them.
  • The API is not very discoverable. The developer is likely looking for methods that have familiar conversion names (As, Cast, etc ...). They will not stumble upon this particular method unless they go through every overload of Create and read the documentation.
  • The API here is named Create which implies an allocation. All other Immutable.Create methods indeed allocate. In this case though no allocation occurs, it simply repackages the underlying array in a new struct.

Additonally the API is lacking in functionality. It has a static up cast member (ImmutableArary.Create<T, TDerived>), a dynamic type check member (ImmutableArray<T>.As) but no dynamic down cast member. Normal array code like the following has no direct mapping in ImmutableArray<T>

object[] array = GetValue();
Dog[] dogArray = (Dog[])array;

Proposed API

The first part of the proposal is to delete ImmutableArray.Create<T, TDerived>. The method name does not line up with its functionality and placing it on ImmutableArray makes it impossible to take advantage of type inference.

The following methods will be added to ImmutableArray<T>:

static ImmutableArray<T> CastFrom<TDerived>(ImmutableArray<TDerived> array)
    where TDerived : class, T;
ImmutableArray<TDerived> Cast<TDerived>();

Putting the methods here both increases their discoverability and allows us to take advantage of type inference. The T value is always known hence the developer only has to specify the target type (as they do with standard arrays). The original sample above becomes:

ImmutableArray<Dog> dogArray = ImmutableArray.Create<Dog>();
ImmutableArray<Animal> animalArray = ImmutableArray<Animal>.CastFrom(dogArray);

Details

CastFrom will have the same functionality as ImmutableArray::Create. It will be non-allocating and not generate any runtime type checks. The constraints on the method are sufficient for the CLR to guarantee the conversion will succeed.

Cast will be non-allocating but will have a dynamic type check. If that type check fails an exception will be generated (in the same manner as a normal C# array conversion).

@terrajobst
Copy link
Member

Excellent write-up. That makes total sense to me. +1

@AArnott
Copy link
Contributor

AArnott commented Jan 12, 2015

👍
Although you have one sentence that seems misleading:

It is possible to convert T[] to U[] in most cases where all elements in the array can convert to U.

The conversion's success has nothing to do with the types of elements in the array, and everything to do with how the array itself was created. A T[] can only convert to U[] if in fact T[] already is a U[] that was previously upcast.

@jaredpar
Copy link
Member Author

@AArnott

I spent probably a good five minutes on that sentence and couldn't find anything I was truly happy with (that didn't expand into a mini version of the C# spec). :(

@weshaggard
Copy link
Member

In general I like the idea and I support the functionality but I'm a little interested to see what others think about the names. We don't have any other methods in the framework called CastUp or CastDown. Perhaps we could call CastUp just Cast and CastDown CastDynamic or something along those lines.

@terrajobst
Copy link
Member

I don't think there are names we could recycle. Cast is wrong in my mind because Linq already provides a method with this name but it's not restricted to being an up-cast -- it will simply perform a direct cast.

Personally, I actually like the proposed names.

@kdelorey
Copy link

CastUp and CastDown makes sense to me. I'm in favor if the proposed names.

@jaredpar
Copy link
Member Author

I made an update to the proposal. @stephentoub pointed out that the way I had CastUp defined wouldn't work because it required additional constraints on the class type parameter T from within a method. In order to work the types involved need to be inverted

  • Class type parameter needs to be the parent type.
  • Method type parameter needs to be the derived type.

The method was changed to reflect this and rename to CastFrom as it seems to be a more appropriate name.

For the second method I personally prefer Cast over CastDown. The reason being that the method can cast up, down and even side ways (between interfaces). The more appropriate name is just Cast.

@terrajobst
Copy link
Member

That makes sense. I agree that in that Cast is the right name. Can you update the proposal then?

@jaredpar
Copy link
Member Author

@terrajobst updated

@weshaggard
Copy link
Member

As a developer trying to use these APIs how am I to figure out that CastFrom actually doesn't allocation without reading documentation? I have a feeling a lot of people will just simply call Cast unless they know what the other one provides. In a case where people call Cast directly with a type can be up cast will it detect this and do the non-allocating version? If so that seems to argue that we just have the Cast method and if not is there a reason?

Another question you are proposing we remove a public API what impact will that have? Have we shipped a stable version with that API?

@jaredpar
Copy link
Member Author

@weshaggard the term cast is generally associated with a type conversion, not an allocation.

Neither of these APIs is allocating, it is just doing a type conversion under the hood. The difference between the methods is CastFrom doesn't require a dynamic type check but Cast does.

@nguerrera or @AArnott would have to comment on whether or not we've shipped with that API public before.

@weshaggard
Copy link
Member

@jaredpar sorry I miss understood the allocation part after re-reading your proposal I think I confused it with the comparison to the other Create methods which allocate.

In your code example what happens if people do this:

ImmutableArray<Dog> dogArray = ImmutableArray.Create<Dog>();
ImmutableArray<Animal> animalArray = dogArray.Cast<Animal>();

That seems to me like the most natural thing for someone to do. If that isn't substantially worse than calling the static CastFrom then why not just have the one Cast method? The fewer concepts people need to figure out the better.

@jaredpar
Copy link
Member Author

@weshaggard the code will end up producing the same result with an additional type check. The reason for adding the extra concept is that this type check can be significant in tight loops.

Roslyn for example regularly executes this exact pattern in the parser:

  • Create an ImmutableArray<ChildNode>
  • Convert to an ImmutableArray<ParentNode>

Profiles showed that the type check in this conversion was taking up a significant portion of parse time. Moving to the non-type checking version was a clear win (and one that we simply cannot regress on).

The awkward consumption of the existing ImmutableArray.Create API is what prompted this request. Roslyn ended up writing their own version which had the basic features called out in this request.

We felt it was enough of a win that it should just be in the core API.

@weshaggard
Copy link
Member

Thanks for the explanation I'm good with the additions. I would like to get clarification from @nguerrera and @AArnott before we remove the one Create method however.

@ellismg
Copy link
Contributor

ellismg commented Jan 13, 2015

In the previous API Review meeting, I think it was mentioned that we had not shipped a stable version of ImmutableArray yet, and were free to make these changes. Note that we just renamed ReverseContents to Reverse with dotnet/corefx#399, so we are already making changes along these lines.

@AArnott
Copy link
Contributor

AArnott commented Jan 13, 2015

ImmutableArray itself has never shipped in a stable package, so we're good
there. And I agree with Jared's other comments.

On Mon, Jan 12, 2015, 10:32 PM Matt Ellis notifications@github.com wrote:

In the previous API Review meeting, I think it was mentioned that we had
not shipped a stable version of ImmutableArray yet, and were free to make
these changes. Note that we just renamed ReverseContents to Reverse with
dotnet/corefx#399 dotnet/corefx#399, so we are already
making changes along these lines.


Reply to this email directly or view it on GitHub
https://github.com/dotnet/corefx/issues/400#issuecomment-69701353.

@nguerrera
Copy link
Contributor

Yes, we can still make breaking changes to ImmutableArray, but we had said that we'd keep them to a minimum if at all possible. I do see a few uses of Create<T, TDerived>, so we'd need to coordinate carefully with our partners.

@davkean davkean changed the title Add CastUp and CastDown to ImmutableArray<T> Add Cast<T> and CastFrom<TDerived> to ImmutableArray<T> Jan 13, 2015
@davkean
Copy link
Member

davkean commented Jan 13, 2015

One comment, the type parameter for Cast should be probably just Cast, to be consistent with Linq, and to indicate that it can go both up and down.

@AArnott
Copy link
Contributor

AArnott commented Jan 13, 2015

Won't Cast hide the lynq extension method? Their behaviors, return types
and cases where they fail are different, so eclipsing one with the other
may be a bad idea.

On Tue, Jan 13, 2015, 11:17 AM David Kean notifications@github.com wrote:

One comment, the type parameter for Cast should be probably just Cast, to
be consistent with Linq, and to indicate that it can go both up and down.


Reply to this email directly or view it on GitHub
https://github.com/dotnet/corefx/issues/400#issuecomment-69801834.

@davkean
Copy link
Member

davkean commented Jan 13, 2015

Return type differences are fine - it's common to provide strongly typed versions of enumerable extensions. How will their behaviors differ?

@jaredpar
Copy link
Member Author

@davkean the LINQ Cast method is casting elements and the ImmutableArray<T> version is casting arrays. Concrete example of where this would differ:

Animal a = new Dog(); 
var array = ImmutableArray.Create(a);  // Creates a Animal[] 
Enumerable.Cast<Dog>(array);  // Successfully returns IEnumerable<Dog> 
array.Cast<Dog>();  // Fail: can't convert Animal[] -> Dog[] 

C# binding rules will cause ImmutableArray<T>.Cast to be preferred over Enumerable.Cast.

@nguerrera
Copy link
Contributor

@jaredpar @davkean @AArnott Given that, I'm inclined to think that we should avoid hiding Enumerable.Cast with something slightly different.

@justinvp
Copy link
Contributor

Given that, I'm inclined to think that we should avoid hiding Enumerable.Cast with something slightly different.

CastTo?

@AArnott
Copy link
Contributor

AArnott commented Jan 14, 2015

CastTo doesn't convey the significant difference IMO.
What about CastArray?

@jaredpar
Copy link
Member Author

@AArnott CastArray is what I've been thinking about as well.

@davkean
Copy link
Member

davkean commented Jan 14, 2015

What's the behavior when the cast doesn't succeed? Can you call this out in the spec?

@jaredpar
Copy link
Member Author

@davkean from the spec

Cast will be non-allocating but will have a dynamic type check. If that type check fails an exception will be generated (in the same manner as a normal C# array conversion).

Is there something else you wanted to say in addition to that?

@davkean
Copy link
Member

davkean commented Jan 14, 2015

Which exception, InvalidCastException?

@jaredpar
Copy link
Member Author

@davkean it throws whatever the array conversion would throw.

jaredpar referenced this issue in jaredpar/corefx Jan 15, 2015
This is the PR that corresponds with API request issue #400.  The issue
has the full details of this change but in summary

Adds ImmutableArray<T>.CastUp<TDerived> static method.  This allows the
caller to convert from an immutable array of derived elements to parent
elements (ex: string to object).  This conversion is statically
verifiable and has no allocation or dynamic type check.

Adds the ImmutableArray.CastArray<TOther> method.  This method is the
immutable array equivalent of casting a standard CLR array to a related
array type.

http://msdn.microsoft.com/en-us/library/aa664572%28v=vs.71%29.aspx

If the conversion fails an InvalidCastException will be thrown.
@terrajobst
Copy link
Member

We've reviewed this proposal and we're fine with this approach. We've some concerns around the naming but this can be reviewed on the PR.

@jaredpar
Copy link
Member Author

jaredpar commented Feb 6, 2015

Fixed via dbfb49b

@jaredpar jaredpar closed this as completed Feb 6, 2015
@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 1.0.0-rtm milestone Jan 31, 2020
@dotnet dotnet locked as resolved and limited conversation to collaborators Jan 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented api-needs-work API needs work before it is approved, it is NOT ready for implementation
Projects
None yet
Development

No branches or pull requests

10 participants