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

Champion "Nullable-enhanced common type" #33

Open
MadsTorgersen opened this issue Feb 9, 2017 · 29 comments
Open

Champion "Nullable-enhanced common type" #33

MadsTorgersen opened this issue Feb 9, 2017 · 29 comments

Comments

@MadsTorgersen
Copy link
Contributor

@MadsTorgersen MadsTorgersen commented Feb 9, 2017

Summary

There is a situation in which the current common-type algorithm results are counter-intuitive, and results in the programmer adding what feels like a redundant cast to the code. With this change, an expression such as condition ? 1 : null would result in a value of type int?.

This and #881 should be taken into the language at the same time.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 10, 2017

To clarify, the example in the Summary condition ? 1 : null would previously need to be written as condition ? 1 : (int?)null.

In the other example, condition ? new Dog() : new Cat(), you state that it "would result in a value of
their common base type Animal." Is there a way to control this?

For example, what if there were an IAnimal interface they both implemented in addition to the base class, and I wanted the result to be of that interface type rather than the abstract Animal type? Could this be influenced by writing this as IAnimal animal = condition ? new Dog() : new Cat()?

@DavidArno

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Feb 10, 2017

What would happen in the following case? Presumably a compiler error as there's ambiguity over which base type to use?

interface IPet {}
interface IMammal {}

class Cat : IPet, IMammal {}
class Dog : IPet, IMammal {}
...
var animal = condition ? new Dog() : new Cat();

Though this could be then solved by doing as @scottdorman suggests:

IPet animal = condition ? new Dog() : new Cat();
@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 10, 2017

@DavidArno In your example, there is no common base class so I think a compiler error is reasonable. In that case, you wouldn't be able to use var and would have to explicitly type it to the interface you want.

You could still use var and just cast each piece to the same interface, like

var animal = condition ? (IPet)new Dog() : (IPet)new Cat();

but that defeats the purpose of the proposal.

@alrz

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Feb 10, 2017

In your example, there is no common base class

object?

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Feb 10, 2017

@scottdorman

In the other example, condition ? new Dog() : new Cat(), you state that it "would result in a value of
their common base type Animal." Is there a way to control this?

Yes. You can cast either operand to the common type that you want to be the result.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 11, 2017

@gafter

Yes. You can cast either operand to the common type that you want to be the result.

So, I'd be able to write this as

var animal = condition ? (IAnimal)new Dog() : new Cat()

but not

IAnimal animal = condition ? new Dog() : new Cat()

That still seems like an unnecessary cast. The fact that I'm strongly typing the variable should be enough for the compiler to know what type it should try to cast Dog or Cat to.

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Feb 11, 2017

@scottdorman The type of a ?: expression does not depend on the context in which it appears.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 11, 2017

@gafter Ok. That makes sense. So in my example, the only viable option is to cast one of the operands to the desired type. In that case, it doesn't feel like I've gained any benefit here.

Now, for the first example (condition ? 1 : null), this would let me skip the explicit cast so less typing/clutter in the code, which is good. :)

@alrz

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Feb 12, 2017

This is expected to affect the following aspects of the language:

Probably ?? would be also affected.

var animal = obj as Dog ?? obj as Cat; 
@DavidArno

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Feb 12, 2017

Looking through the spec of the proposal, it will be a real shame here if the common type were just limited to class types. I would have thought that it would also commonly be the case that the two types share a common interface, but the inheritance common type would be object. In such cases, it would be good to have that interface treated as the common type in such expressions.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 12, 2017

it will be a real shame here if the common type were just limited to class types.

I tend to agree with that statement. It seems to me, as long as there is an unambiguous common type which can be inferred, that type should be used. If that unambiguous common type happens to be an interface, then that's what gets used. If it's ambiguous for any reason (multiple interfaces, etc.) such that the compiler can't clearly decide then it's a compiler error.

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Feb 13, 2017

...such that the compiler can't clearly decide...

Whether or not the compiler "can decide" depends on the rules the compiler is supposed to use to decide.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 13, 2017

Whether or not the compiler "can decide" depends on the rules the compiler is supposed to use to decide.

True, but in the example case where there are two interfaces being implemented:

interface IPet {}
interface IMammal {}

class Cat : IPet, IMammal {}
class Dog : IPet, IMammal {}
...
var animal = condition ? new Dog() : new Cat();

How would the compiler know which interface to use as the common base type? I think in this example, the compiler can't know which one to use and so should raise a compiler error.

However, if we wrote it as

interface IPet {}
interface IMammal {}

class Cat : IPet, IMammal {}
class Dog : IPet, IMammal {}
...
IPet pet = condition ? new Dog() : new Cat();
var animal = condition ? (IPet)new Dog() : new Cat();
IMammal mammal = condition ? new Dog() : new Cat();

Then in the first two instances, the compiler has enough information to know that the common base type is IPet and in the last instance it knows the common base type is IMammal.

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Feb 13, 2017

I honestly think that attempting to determine a common type in those cases should be deferred until after "intersection" types are at least considered. It would be much nicer if the compiler could consider the expression condition ? new Dog() : new Cat() to be of type (IPet & IMammal) than of either specific interface.

@scottdorman

This comment has been minimized.

Copy link
Contributor

@scottdorman scottdorman commented Feb 13, 2017

It would be much nicer if the compiler could consider the expression condition ? new Dog() : new Cat() to be of type (IPet & IMammal) than of either specific interface.

That's an interesting approach, but I think a change like that is a more fundamental change to the type system itself (we can't have a type treated as the intersection of two interfaces right now unless it's done so in the context of an actual base class which implements both of those interfaces).

It also seems like it could introduce some inconsistency as to when the type is derived. If expressions like this cause type resolution to be deferred until after but other expressions don't have that deferred behavior then things can get sticky for the compiler and for us as well as we won't be able to reliably know the "rules" being used.

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Feb 13, 2017

@scottdorman

That's an interesting approach, but I think a change like that is a more fundamental change to the type system itself

I don't disagree, but I think it's worth it. The biggest problem with "most common denominator" between types is exactly the problem of being unable to determine which is the most correct type where interfaces are involved. Otherwise disparate types just boil down to object which isn't particularly useful.

we can't have a type treated as the intersection of two interfaces right now unless it's done so in the context of an actual base class which implements both of those interfaces

Or a generic type parameter with two interface constraints.

@alrz

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Feb 16, 2017

compiler could consider the expression condition ? new Dog() : new Cat() to be of type (IPet & IMammal)

In case you want to hold this feature forever, yes, the compiler should totally do that. I don't see that would happen anytime soon, perhaps we should see if it would break when it's done in a future release after this has been implemented.

On the surface it doesn't look like breaking, because it's a "widening" change, an intersection type would always cover the "most specific common type", yet I can't say for sure as it depends on the details for both proposals.

@Thaina

This comment has been minimized.

Copy link

@Thaina Thaina commented Feb 17, 2017

If we approach with most common denominator but in the future we have union type and intersect type would this feature will be changed?

I mean (Cat | Dog) type

Or we would constraint intersection to only interface?

@DavidArno

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Feb 20, 2017

@alrz,

Would having a (IPet & IAnimal) type come out of a ternary expression really take forever to implement? For the expression:

IPet animal = condition ? new Dog() : new Cat();

The compiler can collapse that multiple common type expression down to IPet within the context of the statement. In the case of:

var animal = condition ? new Dog() : new Cat();

Then it would be a compilation error as there is no one type the compiler can choose for animal. The same compiler error could be applied to other cases, such as method group resolution etc.

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Feb 26, 2017

I am narrowing the spec for this feature to include the handling of c ? 1 : null but it will no longer cover the "common base type" scenario. This is being separated because the former is considered a candidate for 7.1, but the latter is not.

@gulshan

This comment has been minimized.

Copy link

@gulshan gulshan commented Mar 21, 2017

Shouldn't the title be changed to reflect the current narrower scope? It can be something like- "Infer nullability from expression". Or the main "improved common type" discussion can carry on here while a new issue can track the upcoming nullability inference from expression feature.

@alrz

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Apr 10, 2017

Does this affect type inference? for example:

public static T[] ToArray<T>(this (T, T) tuple)
  => new T[] { tuple.Item1, tuple.Item2 };


var array = default((Task<int>,Task<double>)).ToArray(); // should infer T=Task
@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Apr 10, 2017

@alrz This championed proposal only affects the computation of the common type of a value type and a null literal. Please read the proposal itself.

@gafter gafter changed the title Champion "Improved common type" Champion "Nullable-enhanced common type" Apr 20, 2017
@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Sep 5, 2017

We would like to change the spec for this feature to also permit the following:

int? ni = 1;
double d = 4;
bool c = true;
var x1 = c ? ni : d;

This is currently an error because there is no implicit conversion between int? and double in either direction.

See #881

@Thaina

This comment has been minimized.

Copy link

@Thaina Thaina commented Mar 16, 2018

Is this proposal should also include lambda function type guess?

var objWithIndex = items.Select((item,i) => {
    if(item == null);
        return null;
    if(!item.Exist);
        return null;
    return (item,i); // expect to be (T,int)? type
});
@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Mar 16, 2018

Yes

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Aug 21, 2018

The PR #1806 incorporates a solution to #881 into the draft spec for this.

@gafter gafter added this to 8.0 Candidate (not started) in Language Version Planning Mar 6, 2019
@MadsTorgersen MadsTorgersen moved this from 8.0 Candidate (not started) to 8.x Candidate in Language Version Planning Apr 29, 2019
@gafter gafter removed this from the 8.0 candidate milestone Apr 29, 2019
@gafter gafter moved this from 8.x Candidate to 9.0 Candidate in Language Version Planning Aug 28, 2019
@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Aug 28, 2019

This should be done (if at all) at the same time as #2473 and #2460.

@gafter gafter added this to the 9.0 candidate milestone Aug 28, 2019
@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Aug 28, 2019

See also #881

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.