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: Intersection types #4586

Closed
MouseProducedGames opened this issue Aug 16, 2015 · 57 comments
Closed

Proposal: Intersection types #4586

MouseProducedGames opened this issue Aug 16, 2015 · 57 comments

Comments

@MouseProducedGames
Copy link

Let's say you have an IQuadruped interface:

public interface IQuadruped
{
    public Leg FrontLeftLeg { get; set; }
    public Leg FrontRightLeg { get; set; }
    public Leg BackLeftLeg { get; set; }
    public Leg BackRightLeg { get; set; }
    // ...
}

and an IMammal interface:

public interface IMammal
{
    public NumberOfHairs { get; }
    // ...
}

And, of course, some classes implementing both interfaces.

public class Dog : IQuadruped, IMammal, ... { // ... }
public class Cat : IQuadruped, IMammal, ... { // ... }
public class Platypus : IQuadruped, IMammal, IReallyWeird, ... { // ... }
// ...

Now you want to operate on all the quadrupedal mammals in your enumerable of animals:

foreach (var animal in animals.Where(a => a is IQuadruped && a is IAnimal))
{
    IQuadruped qAnimal = (IQuadruped)animal;
    IMammal mAnimal = (IMammal)animal;
    // Code involving both interfaces here.
}

And, of course, the complexity goes up the more interfaces you want to check.

However, if, instead, you had:

foreach (var animal in animals.Select(a => a as I<IQuadruped, IAnimal>).Select(a => !object.Equals(a, null))
{
    // animal is now of type I<IQuadruped, IAnimal>.
    // Any operation supported by either interface is supported by the combined interface.
    // And you only have to reference one variable.
}

In short, I<Interface, ...> combines interfaces into one interface, without having to create an IQuadrupedMammal interface, going back into your code, and refactoring. Which may not be possible, or may be impractical. IQuadrupedMammalReallyWeirdFromAustralia...

@MouseProducedGames MouseProducedGames changed the title Tuple Interfaces Proposal: Tuple Interfaces Aug 16, 2015
@MouseProducedGames
Copy link
Author

I used IAnimal a few times instead of IMammal. Doesn't change the point or anything, but I don't know how to go back and edit out those word errors.

@Corey-M
Copy link

Corey-M commented Aug 18, 2015

This can be done by creating composite interfaces and applying them to the classes:

public interface IQuadrupedMammal : IQuadruped, IMammal
{
}

public class Dog : IQuadrupedMammal { /*...*/ }

for (var animal in animals.OfType<IQuadrupedMammal>())
{
}

Unfortunately this would require you to create such a composite for every combination of interfaces you want to test/use and apply all of the relevant composites to every qualifying class. Especially where you have overlapping composites this has the potential to get out of hand very quickly and certainly damages maintainability.

Perhaps a better alternative is to allow duck typing on interfaces, either in general or by the introduction of a new interface type: implicit interface. I have no idea whether this is possible without changes to the CLR.

@aluanhaddad
Copy link

I think this is a special case for a general feature: Expressing intersection types.
I don't think the proposed syntax would work particularly well, as it would clash with the syntax for generics.
How about

IQuadruped with IMammal with IAnimal

a la Scala or

IQuadruped & IMammal & IAnimal

a la TypeScript?

As a side note, this would be very useful in the context of the proposed pattern matching feature.

@gafter gafter changed the title Proposal: Tuple Interfaces Proposal: Intersection types Oct 21, 2015
@alrz
Copy link
Member

alrz commented Oct 21, 2015

once again, conjunctive patterns would solve this.

if(obj is IQuadruped and IMammal and IAnimal) {}

although, if you want variables, you should define one per interface

foreach (var animal in animals) {
    if(animal is IQuadruped q and IMammal m and IAnimal a) {
        ...
    }
}

I think this is more practical than "type intersections" since C# doesn't allow such syntax in catch clause neither.

@aluanhaddad
Copy link

@alrz the problem with

you should define one per interface

e.g.

if (animal is IMammal m and IQuadruped q and ICarnivore c) { ... }

Is that it is insufficient for many cases.
You would frequently want to have a single variable.

For example suppose I have a method

void ProcessCarnivorousQuadruped<T> (T cq) where T: ICarnivore, IQuadruped { ... }

There is no way to call it from the if block.

At any rate I do not think that the pattern matching proposal needs to cover intersection types. Rather, I think the ability to specify intersection types, as variables, fields, etc. would naturally enrich pattern matching by implication.

@alrz
Copy link
Member

alrz commented Oct 21, 2015

The nice thing about conjunctive patterns is that since they are defined under pattern they would be used in a wider context, while you can already use generic constraints for that case:

void F<T>(IEnumerable<T> animals) where T :  IMammal, IQuadruped, ICarnivore {
    foreach(var animal in animals) {
        ...
    }
}

VB Syntax is even more closer to what you've suggested:

Sub F(Of T As {IMammal, IQuadruped, ICarnivore})(animals As IEnumerable(Of T))
    For Each animal In animals
        ...
    Next
End Sub 

What does type intersections do that can't be done with generic constraints?

@aluanhaddad
Copy link

@alrz the ability to write

void F<T>(IEnumerable<T> animals) where T :  IMammal, IQuadruped, ICarnivore {
    foreach(var animal in animals) {
        ...
    }
}

How do I pass an argument to F?
e.g. how does this work?

object x = ...
switch (x)
{
    case IMammal m and IQuadruped q and ICarnivore c:
    F(???);
}

Generic constraints are wonderful for intersecting multiple types for the callee, but don't do anything for the caller.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

You shall pass the x itself, which (I suppose) is of some type that implements all three interfaces.

@aluanhaddad
Copy link

Right, but how do you pass it. It cannot be cast, since there is again no way to refer to the type. Dynamic would work but that is not the intention.

Also, there are other advantages to being able to write intersection types. Suppose a future version of the language evolves to support Scala style instance level mixins, this would provide a natural way of referring to them.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

You mean it's an object? How would that get there? I would declare an overload for enclosing method of switch so it will be known that it implements those interfaces. If it's upcasted to object what is the point of these interfaces at first place?

@aluanhaddad
Copy link

The idea is you have some general animal handler function like

void ProcessAnimals(IEnumerable<IAnimal> animals) { ... } 

So the type may not be object, but it may not be specific enough. Furthermore, and this was a key point made by @MouseProducedGames in proposing this, suppose you do not have an ICarnivorousMammalianQuadruped interface in the first place.

Even if you did, overloading the enclosing method does not work in any case because you could not pass an IAnimal to it. If you already knew the type statically to the point where you could rely on overload resolution, then using the pattern matching switch would be redundant.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

Well, that is not what you would get from polymorphism. "A polymorphic type is one whose operations can also be applied to values of some other types". If these operations vary from type to type, then you should use virtual dispatch or method overloads to specialize these operations for a specific type.

So, in your example, ProcessAnimals shall be overloaded for operations that cannot be done to a general animal. And the compiler would infer appropriate method from the target type.

@aluanhaddad
Copy link

@alrz then what is the justification for pattern matching on a type at all? Single dispatch object oriented polymorphism has certain inherent limitations. This is one reason why solutions such as pattern matching, the Visitor Pattern, and many others exist. You are basically saying that there are never valid situations where you do not know the type at compile time but you want to treat it differently depending on it's runtime type.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

When you're using record types in patterns, that is called pattern matching, because you're actually matching a pattern like Expr(Const(0), Const(1)) against an object, but what you're doing here is just a type test which of course is base of pattern matching. By the way, F# is using a different syntax for type tests:

match x with
| :? IMammal as m
& :? IQuadruped as q
& :? ICarnivore as c -> ...

@aluanhaddad
Copy link

But the example does not involve a sum type that can be decomposed into different components. Expr(Const(0), Const(1)) is composition not inheritance (the fact that Const implements Expr is irrelevant in this case).

Suppose I have a hierarchy

interface IWord { string Text { get; } }
interface INoun : IWord { IVerb SubjectOf { get; } }
interface IVerb : IWord { INoun: Subject { get; } }

We can agree that nouns and verbs are words, but there are additional properties that are not common between them such that they can be extracted into the base type. Nevertheless it may be valuable to have an IEnumerable<IWord>.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

I have a hierarchy

Yeah, that's inheritance.

Why don't you do this:

void F(IEnumerable<IWord> words) {
    foreach(var word in words) {
        switch(word) {
            case INoun n: ... // operations to nouns
            case IVerb v: ... // operations to verbs
        }
    }
}

If these operations are exactly the same, you should do it on the base IWord.

@aluanhaddad
Copy link

But now, imagine I define the following

interface IPresentPartciple : INoun, IVerb { }

Of course I can define that interface, but there are other types that implement both interfaces but are not present participles and I would like to be able to treat them the same way. With generic constraints I can define a method that processes anything that implements both INoun and IVerb, but I cannot call such a method without casting a word to a specific interface. Which interface is that?

@alrz
Copy link
Member

alrz commented Oct 22, 2015

If there is a "same way" so they both should be derived from the same type. If your class/interface hierarchy was well designed, you wouldn't encounter these problems at all.

So this is very rare, but if it happens, conjunctive patterns.

@ufcpp
Copy link
Contributor

ufcpp commented Oct 22, 2015

I like this concept, but IMO, its syntax should be consistent with that of generic constraints. How about the following:

x is T t where T : IQuadruped, IMammal

@aluanhaddad
Copy link

@alrz In many cases, that would be symptomatic of an ill thought out hierarchy, but sometimes you have multiple axes of inheritance that cut across one another due to the semantics of the domain.

At any rate deriving such combinations from a base type, here it could be INounAndVerb, is possible, even easy, but it is unpleasant because you have to define and extend an interface for every combination. That aside, conjunctive patterns, as you propose them, do not suffice because they do not give you a single reference to the matched value.

Also the number of combinations can get out of hand quickly.
Going back to the original example:

interface IAnimal { }
interface IReptile : IAnimal { }
interface IMammal : IAnimal { }
interface ICarnivore : IAnimal { }
interface IMammalLikeReptile : IReptile, IMammal { } // of paleontologic interest
interface ICarnivorousReptile : IReptile, ICarnivore { }
interface ICarnivorousMammal : IMammal, ICarnivore { }
//.. and on and on 

@ufcpp that seems quite nice.

@alrz
Copy link
Member

alrz commented Oct 22, 2015

Which ones you want to combine then?

@aluanhaddad
Copy link

Quite possibly all of them, but it will evolve as functionality increases.
if I add

interface ICarnivorousMammalLikeReptile : IMammalLikeReptile, ICarnivore { }

I want to be able to match the type of carnivorous reptiles against it.
Which requires me to write

interface ICarnivorousMammalLikeReptile : 
    IMammalLikeReptile, 
    ICarnivorousReptile, 
    ICarnivorousMammal { }

@alrz
Copy link
Member

alrz commented Oct 22, 2015

What kind of operation that would it be? That doesn't make sense at all. You are just mentioning types and combinations. Not operations.

@aluanhaddad
Copy link

How does it not make sense? Perhaps I have some logic that takes into account aspects of reptiles in determining their likelihood of successfully catching prey. It's an example, but there are use cases. If conjunctive patterns make sense, then it makes sense for something to implement a variety possibly disparate interfaces, so the combinations make sense by implication.

Anyway, there are languages that have this feature, for example TypeScript using syntax

T & U

and Scala using syntax

T with U

@alrz
Copy link
Member

alrz commented Oct 22, 2015

of reptiles

So the method your are looking for would be defined in the IReptile interface. Hence, you won't need any combination.

@aluanhaddad
Copy link

Negative it would be a generic method

AnalyzeReptilianHuntingBehavior<T>(T hunter) where T: IReptile, ICarnivore { ... }

And I may pass an ICarnivorousMammalLikeReptile or an ICarnivorousReptile to it by using pattern matching or type testing to determine that the actual value implements both interfaces but not needing to know which one combination interface it implements. Otherwise, I have to cast and the cast may fail so I have to cast against trying all known combinations, and it will break if new combinations area added.

@lmcarreiro
Copy link

What happens in the case that two interfaces when both of them contains a member with the same name, but with different implementations (explicit implementations)?

I think that it would be complicated, even if the implementations are the same (implicit implementation), because the member gets duplicated in the class's method table... at runtime there is no difference, CLR doesn't know if the implementations are the same or not.

@HaloFour
Copy link

@lmcarreiro

Personally I'd go with overload resolution in the order in which the intersected interfaces are defined:

public interface IFoo {
    void Hello();
}

public interface IBar {
    void Hello();
}

public class FooBar : IFoo, IBar {
    public Hello() {
        Console.WriteLine("Foo!");
    }

    void IBar.Hello() {
        Console.WriteLine("Bar!");
    }
}

(IFoo && IBar) test1 = new FooBar();
(IBar && IFoo) test2 = new FooBar();

test1.Hello(); // prints Foo!
test2.Hello(); // prints Bar!

I can appreciate that there may be some confusion that IFoo && IBar and IBar && IFoo wouldn't behave identically, though.

@alrz
Copy link
Member

alrz commented Aug 18, 2016

This situation is currently reproducible,

interface A {void M();}
interface B {void M();}

void M<T>(T t) where T : A, B => t.M(); // ERROR

The only place that I've encountered in which order becomes significant is with covariants (example). In that case, the compiler can not possibly know about the ambiguity.

@Pzixel
Copy link

Pzixel commented Dec 26, 2016

It would be a great feature. For example I'm using some code-generating method which returns class for specific inerface:

public T GenerateMyClass<T>(){...}

But if my inner class is for example IDisposable I have only two possibilities

  1. Just implement it and return base interface. It should be manually casted to IDIsposable to free resources or
  2. I have to force my users to inherit IDisposable interface themselfs by using where T : IDisposable even when T should not be IDisposable.

With this feature it would be easy to write:

public T & IDisposable GenerateMyClass<T>(){...}

which would accomplish it without any cons.

@TonyValenti
Copy link

I really like the following syntax for declaring variables:

var x is IAnimal, IMammal = Something();

if(y is IAnimal, IMammal Z){
    ...
}

As noted above, this keeps the syntax similar to that of constraints.

@Pzixel
Copy link

Pzixel commented Jan 4, 2017

@TonyValenti it's ruining current C# style so it won't be applied. We have two syntaxes: C-like when you declare type and then variable name, and Pascal-like, when you are specifying it after variable name. The latest is used in languages with strong type inference, and C# 1.0 wasn't it. Thus, we will have C-style until C#'s death.

So answering your question, it's just easy to forbid explicit type specification in those cases (like it is for anonymous types). So it's qute easy:

var x = (object) Something();
var y = (IAnimal, IMammal) x;
var z = x as IAnimal, IMammal;

@Pzixel
Copy link

Pzixel commented Jan 6, 2017

@HaloFour

The compiler would emit a local for each type, assign them all to the same instance and silently switch between the locals depending on the context of its use. I initially thought that this might be an issue, especially if y was passed to a method as ref, but since pattern variables are readonly I don't think that would be an issue.

But what if we call a method where both aspects are required? I think this feature should generate intersection interface for every place where we are using this syntax and add it to all classes which implements both of them. It's probably better to generate interface on the fly in runtime but it doesn't matter, because what matters is that it defenitly solves the problem with, for example, IDisposableList:

if (a is (IList<int>, IDisposable) disposableList)
{
   Consume(disposableList);
}

...

public void Consume<T, TItem>(T list) where T : IList<TItem>, IDisposable
{
    var index = list.IndexOf(default(TItem));
    list.Dispose();
}

There is no way to do it with silent switch

@HaloFour
Copy link

HaloFour commented Jan 6, 2017

@Pzixel

It'd be doable, but it'd be messy. I don't know that the compiler would have a choice but to resort to reflection to obtain the open generic type/method and then close it with the original type of a. Then the compiler would invoke the member dynamically. It would carry a bit of overhead.

Another option would be for the compiler to emit a proxy type which does meet the constraint and to use the generic type/method with that proxy type. That would eliminate the overhead of reflection but the consuming code would be dealing with a different type which could have other unwanted consequences.

I don't see how the compiler could ever support that correctly without the CLR understanding the concept of intersection types. Otherwise there's no way for the compiler to correctly close that generic at compile-time.

@jnm2
Copy link
Contributor

jnm2 commented Jan 6, 2017

I'm pretty sure that this would just end up being frustrating without CLR support.

@Pzixel
Copy link

Pzixel commented Jan 6, 2017

@HaloFour I see your point. For example we may ask for if (a is (IFoo, IBar) foobar) and behaviour may differs based on if there is IFooBar interface and we can use it or we should generate another one? I think there is simple answer: just generate a proxy interface always. It's fine with anonymous types, why not generate anonymous interfaces? If user whant specific interface it always can manually cast to IFooBar instead of (IFoo, IBar), so if it writes this code, it's explicitely asks us to generate some proxy interface for him just because he is too lazy to write IQuadrupedMammalReallyWeirdFromAustralia for every combination of interfaces required.

@HaloFour
Copy link

HaloFour commented Jan 6, 2017

@Pzixel

I don't understand what a generated composite interface would accomplish? If the underlying type doesn't implement that interface it still couldn't be cast to it or used as a generic type argument.

That also doesn't take into account the possibility of base types being included in the intersection.

@Pzixel
Copy link

Pzixel commented Jan 6, 2017

@HaloFour it would accomplish situation, when you need methods from interfaces IA and IB, but there is no common divider for them. And underlying type can't implement an interface which is does not exist yet. And this is exactly what we are trying to do: allow compiler to generate this interface for us. You can specify I take everything that implements IFoo and IBar but you cannot pass a variable of type Foo and Bar. It's not fair I think.

That also doesn't take into account the possibility of base types being included in the intersection.

That's not true. I said Always generate another interface. So if there is already an IFooBar interface we just ignore it because it may add additional methods which is NOT what type intersection is.

@HaloFour
Copy link

HaloFour commented Jan 6, 2017

@Pzixel

I still don't understand what you're proposing there. Are you suggesting that if you try to convert to (IFoo, IBar) that the compiler would automatically emit a new composite interface which implements both interfaces as well as a proxy class which implements the composite interface and dispatches the methods to the underlying type?

And I don't understand your response about base types. Are you suggesting that base types cannot participate in type intersection? Why not?

public interface IFizz { ... }
public class Foo { ... }
public class Bar : Foo, IFizz { ... }

object o = new Bar();
if (o is (Foo, IFizz) fooFizz) {
    ...
}

How would compiler-generated composite interfaces or proxies help there?

@Pzixel
Copy link

Pzixel commented Jan 6, 2017

@HaloFour no, I'm suggesting proxy-interface generation only.

For example I write following method:

public void Consume<T, TItem>(T list) where T : ICollection<TItem>, IReadOnlyList<TItem>
{
    bool hasDefaultItem = list.Contains(default(TItem));
    var something = list[10];
}

We can not use it with type List<T>, because it implements both ICollection<T> and IReadOnlyList<T>. We also have our own class which implements them:

class CollectionReadOnlyList<T> : ICollection<T>, IReadOnlyList<T>
{
    ...
}

Now the question is how can we call this method?

object obj = GetListOrOurOwnCollection();
var collectionAndReadOnlyList = obj as ???
if (collectionAndReadOnlyList != null)
{
   Consume(collectionAndReadOnlyList )
}

I propose automatic generation of interface intersection which allows us to write something like:

object obj = GetListOrOurOwnCollection<int>();
if (obj is (ICollection<int>, IReadOnlyList<int>) collectionAndReadOnlyList)
{
    Consume(collectionAndReadOnlyList);
}

Today we only can do something like:

object obj = GetListOrOurOwnCollection<int>();
if (obj is List<int> list)
{
    Consume<List<int>, int>(list);
}
else if(obj is CollectionReadOnlyList<int> ourOwnCollection)
{
    Consume<CollectionReadOnlyList<int>, int>(ourOwnCollection);
}
else if (obj is SomethingElseThatImplementsThoseInterfaces<int> somethingElse)
{
    Consume<SomethingElseThatImplementsThoseInterfaces<int>, int>(somethingElse);
}
...

We can't modify built-in collection and make List<T> implement CollectionReadOnlyList<T>, but it obviosly implements it, because if it implements ICollection<T> and IReadOnlyList<T>, it defenitly implements both. So we don't have to write it explicitely, compiler is great in such things like this one, it can infer all combinations of all interfaces inherited.

@HaloFour
Copy link

HaloFour commented Jan 6, 2017

@Pzixel

We can't modify built-in collection and make List<T> implement CollectionReadOnlyList<T>, but it obviosly implements it, because if it implements ICollection<T> and IReadOnlyList<T>, it defenitly implements both.

Not according to either C# or the CLR. List<T> would be required to directly implement ICollectionReadOnlyList<T> itself, it doesn't matter that it already implements ICollection<T> and IReadOnlyList<T> separately.

The only way that would work would be if the C# compiler also created a proxy class which implements this generated composite interface and then dispatched all of the member calls to the underlying reference. But then you're no longer dealing with List<T>.

@jnm2
Copy link
Contributor

jnm2 commented Jan 6, 2017

The only way that would work would be if the C# compiler also created a proxy class which implements this generated composite interface and then dispatched all of the member calls to the underlying reference. But then you're no longer dealing with List.

Yes, and imagine the side effects: just by casting to an intersection, you end up passing a different instance around. Even worse if this instance swapping is done implicitly.

And it still doesn't work for intersections that include a class type. It could be sealed or not have a parameterless constructor.

CLR all the way.

@Pzixel
Copy link

Pzixel commented Jan 6, 2017

@HaloFour I got your point. There is no way to object to be casted to an interface which was not added at compile time. Thus this feature is not backward-compatible and thus will be defenitly rejected.

P.S. Now I see that it's really hard to enhance a language without breaking existing things down...

@aluanhaddad
Copy link

I agree this needs CLR support to be viable.
I really want this feature as I've indicated in many comments, but CLR support is a must.
I really hope the next version of the language looks into questions like this and seriously considers this and many other features that require changes to the CLR.

@jnm2
Copy link
Contributor

jnm2 commented Sep 27, 2018

Would you please close, since language design discussions have moved to https://github.com/dotnet/csharplang? dotnet/csharplang#399 seems to be the csharplang counterpart.

@gafter gafter closed this as completed Sep 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests