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

Proposal - Ternary Operator should resolve types that are of the same base type #6814

Closed
mirhagk opened this issue Nov 17, 2015 · 20 comments
Closed
Assignees
Labels
2 - Ready Area-Language Design Resolution-Duplicate The described behavior is tracked in another issue

Comments

@mirhagk
Copy link

mirhagk commented Nov 17, 2015

If you have

class Animal {}
class Dog : Animal {}
class Cat : Animal {}

You should be able to write return likesCats ? new Cat() : new Dog(); which would return an object of type Animal.

This should fall under the better betterness category where the compiler can figure out what the author clearly meant to do, and a cast to a base type is a safe implict cast that isn't introducing any side effects. It's also simply fixing code which was not allowed before, so it is backwards compatible (this rule of determining the type should run last, in case the types can already be implicitly converted)

This should be restricted to the case where the two expressions share a common base class, and it should choose the highest base class applicable (ie Animal not Object should be chosen). This avoids situations where the expressions could both be converted to multiple interfaces (ie if they both derived from a class that defined IDisposable then they could be casted to an IDisposable but this would be a less useful type and cause conflicts).

Potential points of discussion:

  • Should object base type be excluded from this? Everything derives from object which would make every ternary operator valid but returning a less than useful type most of the time. Most likely the user didn't want to do this, but had an error like calling the wrong method or something. The potential error of the invalid cast would likely be caught at the very next level when it tries to return or assign it but this may not always be the case. If the result of this expression went into a var and then was used to call a generic method or instantiate a generic class then this may go pretty far in the program before producing a compile time error, and may not introduce one at all.
  • If object is a special case should there be more special cases? Or is it worth having if there are special cases and hence added complexity?
@mirhagk mirhagk changed the title Ternary Operator should resolve types that are of the same base type Proposal - Ternary Operator should resolve types that are of the same base type Nov 17, 2015
@HaloFour
Copy link

I think this comment from #6789 would be relevant here:

C#'s "common type" algorithm (as currently specified) never produces a type that is not one of the input types.

@mirhagk
Copy link
Author

mirhagk commented Nov 17, 2015

Yes that's true, and if that is a conscious decision then perhaps this isn't worth going against that rule of thumb. But it's a fairly obvious type resolution here.

It's also one of those weird gotchas I've seen people come across quite a few times. Especially since people logically think of the ternary operator like an inline if statement but this type resolution violates that. For instance:

Animal Foo(bool likesCats)
{
    if (likesCats)
        return new Cat();
    else
        return new Dog();
}

Can't be made into return likesCats ? new Cat() : new Dog(); and it's a total gotcha moment when people see the latter give a syntax error even though in their minds the code is exactly the same.

@gafter
Copy link
Member

gafter commented Nov 17, 2015

Are you suggesting a change for ternary only, or for all the places that 7.5.2.14 (Finding the best common type of a set of expressions) is used as well?

@bondsbw
Copy link

bondsbw commented Nov 17, 2015

@gafter I can't speak for @mirhagk but I would personally appreciate this change in the areas I can think of at the moment:

  • ternary expressions
  • implicitly typed array creation expressions new [] { new Cat(), new Dog() }
  • type parameter inference with lambda body blocks void M<T>(F<bool, T> f){...}, supplying b => { if (b) return new Cat(); else return new Dog(); }

@alrz
Copy link
Contributor

alrz commented Nov 17, 2015

Also match expressions and disjunctive patterns would benefit from this change.

@orthoxerox
Copy link
Contributor

@mirhagk

interface IFoo { ... }
interface IBar { ... }
class A : IFoo, IBar { ... }
class B : IFoo, Ibar { ... }

What should return randomBool() ? new A() : new B() return? An object? An IFoo? An IBar? Or should we ask for intersection types (IFoo&IBar) a la Ceylon and Crystal as well?

@bondsbw
Copy link

bondsbw commented Nov 17, 2015

@orthoxerox I would be ok only extending this inference to non-interfaces only, so the common type in your example would be object.

But if C# and the CLR were to support intersection types then by all means, let them join the party.

@TonyValenti
Copy link

@bondsbw @gafter -
I have not heard of the term "intersection types" before, but after doing a little bit of Googling, it seems like this is something I've been wanting for quite a while and they are already somewhat there already. For a long time, I have felt like the return type of an expression should be, not a single type, but the intersection of all possible output types (obviously, everything intersects with "object" and should only be used if there are no other possibilities).

For example, let's say you have the following classes:

interface IWild { } //It is a wild animal
class Animal {}

class Dog : Animal {}
class Wolf : Dog, IWild { }

class Cat : Animal {}
class Tiger : Cat, IWild { }

if I say:

var items = new [] {new Dog(), new Cat() };

this would be the same as:

var items = new Animal[] {new Dog(), new Cat()};

But, if I were to say:

var items = new [] {new Wolf(), new Tiger()};

this would be the similar to sayin saying (by the way, this doesn't work today):

public T[] Items<T>(params T[] values) where T:Animal, IWild {
  return values;
}
var items = Items(new Wolf(), new Tiger());

You'll see that in this case, the return type, T is both Animal and IWild .

I have wanted to be able to express constraints/modifiers on regular variables ever since "dynamic" was first introduced. For me, whenever I'm working with dynamic, it is usually on an object that has known and unknown members. For the known members, I want the safety of being able to "dot" into them and for the unknown members, I want "dynamic" to be used for them. To get around that, I usually have a "typed" reference and a dynamic reference to the same variable. I've had a few ideas for expanding syntax to handle a few different scenarios:

//Different syntaxes for creating a list of items that are IAnimal and IPet:
var items = new List<IAnimal IPet>();
var items = new List<IAnimal & IPet>();
var items = new List<T>() where T:IAnimal, IPet;

//Creating an object that has dynamic and non-dynamic members:
dynamic Person user = new UserPerson(); 

//Creating a variable that contains a type that must satisfy two different types:
IAnimal IWild animal = ....; 
IAnimal & IWild animal = ....; 

//Creating a variable that can never be assigned null.
not-nullable IAnimal animal = new Hedgehog();

@HaloFour
Copy link

@TonyValenti

This proposal seems to be more of a change to how the compiler infers the type of an expression, calculating a common type from a list of potential types.

Some of your requests are covered to some extent by the following existing proposals:

#227 Proposal for non-nullable references (and safe nullable references)
#2146 Proposal: Implicit Interfaces
#3012 Meta-Contracts for Dynamics (For Tooling Improvements)
#4586 Intersection Types

@mirhagk
Copy link
Author

mirhagk commented Nov 17, 2015

@orthoxerox As I mentioned it should ignore interfaces as it would cause conflicts in many situations. (Anything that inherited from something that implemented IDisposable would instantly conflict). So for your example it would indeed by of type object

@gafter I think I agree with @bondsbw in that those places should also do it. Specifically the lambda case, as that's another instance where you'd just expect it to work since it works fine as a named function.

Although looking at the C# 5 spec I see that the ternary operator has a separate definition for type resolution, and does not reference the rule for 7.5.2.14. 7.5.2.14 does describe it's type inference as basically being the same as determining T in void M<T>(params T[] values), so perhaps the type resolution change would also affect that case but I think that'd mean it'd affect this case as well:

IEnumerable<T> Append<T>(IEnumerable<T> list, T item)
{
    return list.Union(new T[] { item});
}

var cats = new List<Cat>();
var dog = new Dog();
Append(cats,dog); //returns a List<Animal> now

Which may or may not actually be wanted (we're stepping into covariance and contravariance territory now and I'll be the first to admit that type theory isn't my strong point)

@Joe4evr
Copy link

Joe4evr commented Nov 17, 2015

@TonyValenti Minor nitpick, but Wolf deriving from Dog and Tiger from Cat doesn't make a lot of sense. A more accurate hierarchy would look like this:

interface IWild { } //It is a wild animal
class Animal {}
class Mammal : Animal {}

class Canine : Mammal {}
class Dog : Canine {}
class Wolf : Canine, IWild {}

class Feline : Mammal {}
class Cat : Feline {}
class Tiger : Feline, IWild {} //I'd also throw 'abstract' on everything but the lowest level classes, but that's just me

😜

@aluanhaddad
Copy link

@mirhagk ignoring interfaces when determining the best common type would make this enhancement useless in a large percentage of cases.

As noted by @HaloFour there are a number of existing proposal, specifically #4586 intersection types, which touch on or are directly related to this issue. I think intersection types would be a powerful concept which would allow existing features, such as the ternary syntax, to be enhanced while providing a context to assist in introducing potential new features, such as mixins, in the future.

Unfortunately, the current proposal, #4586, does not introduce or present the concept or the use cases clearly. It does not really represent a concrete proposal and provides neither sufficient syntactic nor sufficient semantic information. The original proposer @MouseProducedGames has not participated in it actively for a number of months.

I have started writing my own proposal, but I'm not sure if it would be a appropriate to open a redundant issue.

@bondsbw
Copy link

bondsbw commented Nov 19, 2015

@aluanhaddad I like intersection types, but I would prefer to go ahead with class-only inference in the event that intersection types become delayed or are never implemented.

Curious... without intersection types, would it be possible to still infer the interface if there were only one common most-derived interface (assuming the only common superclass is object)?

@aluanhaddad
Copy link

@bondsbw I imagine that it would be just as possible to infer a single common interface as a single common class. This is all speculative, as there is no analogous type inference in C# at present.
Intersection types are actually an orthogonal issue, but if they existed they would allow the type inference process to capture and represent a much richer, more meaningful common type.

Curious... without intersection types, would it be possible to still infer the interface if there were only one common most-derived interface (assuming the only common superclass is object)?

Intersection types have no impact on the technical capability to infer an interface vs a class. In the context of variable declarations, inferred or otherwise, they allow one to have a reference to a value which is typed as some aggregate of the types it implements/extends. It's also worth noting that there is not really a notion of a most derived interface. The candidates for interface inference would be all interfaces implemented by all of the values in question, in this case the two values of the ternary expression. (Edit: I guess if there are interfaces that extend other interfaces in the set of types there could be a most derived one depending on how many axes of interface implementation are present on the type in question.)

The type inference process could examine all of the types implemented by both values and could infer the most specialized (not most derived) common type as a type which implements the subset of all the types they have in common. However, there is no way to express that type in the language. Intersection types give you just that expressiveness, which is why they are relevant here.

Edit: A helpful way to think about intersection types is to compare them to generic constraints. If I have a method:

void M<T>(T value) where T: ISomeInterface, ISomeOtherInterface => ... 

then value must be of some type T which implements both ISomeInterface and ISomeOtherInterface so the body of M may call any method of either interface and may use value in a context where either or both are required.
Now consider I want to call M and lets say I have a value of some unknown type and I test that it implements both interfaces using is, even if it does, I cannot pass it to M because I cannot refer to it correctly. Intersection types would allow that. Naturally, they have lots of other uses as well.

@mirhagk
Copy link
Author

mirhagk commented Nov 19, 2015

I agree that ideally it would solve the interface issue, but with interfaces it's possible to have multiple types that do not inherit from each other as totally valid types.

abstract class Animal{}
interface IWilid{}
interface ICapableOfFlight{}
abstract class Bird: Animal{} //not all birds are capable of flight
class Loon : Bird, IWild, ICapableOfFlight
class Eagle : Bird, IWild, ICapableOfFlight

return isCanadian ? new Loon() : new Eagle();

Then the type could equally validly be Bird, IWild, ICapableOfFlight.

I'm thinking that the only real way to resolve this would be to have intersection types, so perhaps the solution until those are implemented could simply be to throw an error in situations like this. It would still resolve all of the simple cases, and it'd even give a more helpful error message. Perhaps something like:

Could not resolve type of expression, could be Bird, IWild, ICapableOfFlight

It'd be something where the user would even understand why that doesn't work.

@bondsbw
Copy link

bondsbw commented Nov 20, 2015

@aluanhaddad Again, I'm not against intersection types by any means. I want them in the language.

I would just prefer to have this feature regardless of whether intersection types are ever implemented. Adding intersection types later would still be possible and should still support this feature.

@mirhagk

I'm thinking that the only real way to resolve this would be to have intersection types.

The return type could resolve to only Bird, for now (until intersection types are implemented). Wouldn't that be a better experience than an error?

@aluanhaddad
Copy link

@bondsbw I agree. I'm not against this proposal. I apologize if I gave the impression I was.

@dsaf
Copy link

dsaf commented Dec 31, 2015

Duplicate (subset) of #1419 - even the sample taxonomy is identical :).

@gafter
Copy link
Member

gafter commented Dec 31, 2015

@dsaf Agreed; we'll treat this as a duplicate of #1419. @mirhagk Is there any aspect of this that is missing from #1419?

@gafter gafter closed this as completed Dec 31, 2015
@gafter gafter added the Resolution-Duplicate The described behavior is tracked in another issue label Dec 31, 2015
@mirhagk
Copy link
Author

mirhagk commented Jan 1, 2016

Looks like everything is covered in #1419, I'll just add my two cents there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 - Ready Area-Language Design Resolution-Duplicate The described behavior is tracked in another issue
Projects
None yet
Development

No branches or pull requests

10 participants