-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Comments
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. |
This can be done by creating composite interfaces and applying them to the classes:
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: |
I think this is a special case for a general feature: Expressing intersection types. 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. |
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 |
@alrz the problem with
e.g. if (animal is IMammal m and IQuadruped q and ICarnivore c) { ... } Is that it is insufficient for many cases. 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. |
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? |
@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 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. |
You shall pass the |
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. |
You mean it's an |
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 Even if you did, overloading the enclosing method does not work in any case because you could not pass an |
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, |
@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. |
When you're using record types in patterns, that is called pattern matching, because you're actually matching a pattern like match x with
| :? IMammal as m
& :? IQuadruped as q
& :? ICarnivore as c -> ... |
But the example does not involve a sum type that can be decomposed into different components. 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 |
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 |
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? |
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. |
I like this concept, but IMO, its syntax should be consistent with that of generic constraints. How about the following:
|
@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 Also the number of combinations can get out of hand quickly. 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. |
Which ones you want to combine then? |
Quite possibly all of them, but it will evolve as functionality increases. interface ICarnivorousMammalLikeReptile : IMammalLikeReptile, ICarnivore { } I want to be able to match the type of carnivorous reptiles against it. interface ICarnivorousMammalLikeReptile :
IMammalLikeReptile,
ICarnivorousReptile,
ICarnivorousMammal { } |
What kind of operation that would it be? That doesn't make sense at all. You are just mentioning types and combinations. Not operations. |
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 |
So the method your are looking for would be defined in the |
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. |
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. |
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 |
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. |
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
With this feature it would be easy to write: public T & IDisposable GenerateMyClass<T>(){...} which would accomplish it without any cons. |
I really like the following syntax for declaring variables:
As noted above, this keeps the syntax similar to that of constraints. |
@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; |
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, 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 |
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 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. |
I'm pretty sure that this would just end up being frustrating without CLR support. |
@HaloFour I see your point. For example we may ask for |
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. |
@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
That's not true. I said |
I still don't understand what you're proposing there. Are you suggesting that if you try to convert to 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? |
@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 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 |
Not according to either C# or the CLR. 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 |
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. |
@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... |
I agree this needs CLR support to be viable. |
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. |
Let's say you have an IQuadruped interface:
and an IMammal interface:
And, of course, some classes implementing both interfaces.
Now you want to operate on all the quadrupedal mammals in your enumerable of animals:
And, of course, the complexity goes up the more interfaces you want to check.
However, if, instead, you had:
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...The text was updated successfully, but these errors were encountered: