Discussion: Union and Intersection types #399
Replies: 170 comments 41 replies
-
Well, they're awesome in Typescript! For a good example of how the syntax plays with a more C-style language (like C#), look at the docs for Ceylon (https://ceylon-lang.org/documentation/1.3/tour/types/). It has all the compiler support for union / intersection types but with a syntax much closer to C#'s. One of the unexpected benefits I saw of union types is that you can now enumerate a tuple. Normally you can't do that because the types are heterogeneous but with union types Types now have the same behaviour as sets when you apply the union or intersection operations such as commutativity, associativity etc. For example, |
Beta Was this translation helpful? Give feedback.
-
Quite confused by the usages. Keep in mind that C# is an IL-level strongly-typed language. For example,
There's quite different logic for looking-same occasions, so I think this would better be a sugar for compiler. And for
In fact, it's good for the compiler to keep "possible types for a base-type object" as a contract for code analysis, but "intersection type" is totally incompactable for strongly-typed variable platform. You got this idea from TypeScript, but it's like an analysis system for JavaScript, the platform that variables have no type. |
Beta Was this translation helpful? Give feedback.
-
@huoyaoyuan I think this simple rule I mentioned mitigates most of your concerns-
I myself is not yet sure about the usefulness of union types as return types of methods. I would not support it in the first phase, if union types are introduced. I mentioned it as a possibility nevertheless. |
Beta Was this translation helpful? Give feedback.
-
I think that union types are the natural next step after Tuples. If a Tuple<string, int> can be thought of as the Cartesian product of the two sets Intersections are interesting but probably not particularly useful (at least I can't think of many uses right now), but it may be the case that by implementing union types the intersection types will naturally follow. Again, to see the value of these new type expressions, I recommend reading the docs of the Ceylon language. |
Beta Was this translation helpful? Give feedback.
-
@Richiban wrote:
There's a somewhat famous (at least in the Scala community) article by Miles Sabin, showing how to implement union types (which Scala doesn't have) using intersection types (which Scala does have) by virtue of the Curry-Howard-Isomorphism and DeMorgan's Laws. Since both the Curry-Howard-Isomorphism and DeMorgan's Laws "work in both directions", I wouldn't be surprised to learn that once C♯ gets union types, intersection types could be implemented in a similar fashion anyway, so one might just add them to the language anyway. By the way, intersection types are used all over the place in Scala. |
Beta Was this translation helpful? Give feedback.
-
I prefer this style. |
Beta Was this translation helpful? Give feedback.
-
I would like to know some opinion from everyone about my proposal-
How much does it fits with usage of union and intersection types already in use in other languages? |
Beta Was this translation helpful? Give feedback.
-
@gulshan I don't think that's a feature that makes sense; you should be able to union or intersect any two types you wish. As to whether it's technically possible: I don't know. There might be some implementation detail that leaks through in the fact that you can't perform these operations on all type types (?). The only two languages I know of that support union & intersection are Typescript and Ceylon; neither of them place any restriction on what types are supported. For example, this should make perfect sense: public IEnumerable<string>|string GetSomeStrings()
{
// ...
} |
Beta Was this translation helpful? Give feedback.
-
The other really nice thing about union types is they give you a great way of representing null--by giving the literal public interface IPersonRepository
{
Person|null Retrieve(PersonId personId);
} |
Beta Was this translation helpful? Give feedback.
-
@Richiban I know it is incredibly nice! TypeScript has its flaws, but it keeps making me jealous as I watch it just nailing concept after concept that could never become part of C#. |
Beta Was this translation helpful? Give feedback.
-
Life is easier in TypeScript where you only really have one actual type in the type system (excluding a few primitives) and everything else is window dressing. You could do the same with C#, but you'd have to erase all the unions and simply have the type object everywhere. That means always boxing all the struct types. That means the type |
Beta Was this translation helpful? Give feedback.
-
Why not secretly lift the type in question into an Either<T1, T2> struct? This way code would be both backwards compatible and performant and the syntax of the language would be able to consume such a type as if it was a real union. |
Beta Was this translation helpful? Give feedback.
-
Can we then finally get the following: public interface IOperation
{
IDisposable & IObservable Start();
} |
Beta Was this translation helpful? Give feedback.
-
@Richiban The union example was nice. Any use of intersection types for classes you can think of in C#? |
Beta Was this translation helpful? Give feedback.
-
@gulshan I think that @tpetrina 's post above has a good intersection example. |
Beta Was this translation helpful? Give feedback.
-
This is an important feature for swift interop.
This works for arguments because the caller owns the type of T. It does not work for return values because the callee owns the type of the return type so it can't presently be expressed in C#. |
Beta Was this translation helpful? Give feedback.
-
I would love having this feature available in c#. It makes your life easyer. |
Beta Was this translation helpful? Give feedback.
-
I'd love to see function definitions like this: public (int|byte|string) bar() {...} And consume like this: var foo = bar(); switch(foo) { And compile time error if the definition of bar adds or removes type definitions as acceptable return types and it isn't handled in the switch. |
Beta Was this translation helpful? Give feedback.
-
There have been a lot of discussions about this and I found them very useful. But when will this language feature get implemented? |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
I haven't seen mention of this use case from a quick Ctrl-F on this thread, so I wanted to jot it down here. If I'm overlooking some robust solution that's viable with today's tools and provides a similar quality of user experience, please reply here and let me know (selfishly, it would help with the actual specific use case in an internal utility that brought me to this thread 😈).
Having union types fully integrated into the type system would significantly improve my experience in this case. The type parameter could be something that expresses that the report is going to provide any one of the following:
Whenever the caller opts-in to passing in something useful for their |
Beta Was this translation helpful? Give feedback.
-
I would like to mention another important utility of the feature.
For the example above, I want to use covariance and store the |
Beta Was this translation helpful? Give feedback.
-
I posted this comment elsewhere but since thread was created earlier, I'm posting it here to - not meaning to spam at all. One approach without runtime support but with strong typing is creating multiple parameters - one for each requested type, in metadata/IL, with special attributes. Then the language can interpret it and "make it look" like it's an intersection type: void Foo(IDog & ICat dogCat)
{
dogCat.Bark();
dogCat.Meow();
}
var dogCat = new DogCat();
Foo(dogCat);
// becomes:
void Foo([IntersectsWith(nameof(dogCatCat))] IDog dogCatDog, [IntersectsWith(nameof(dogCatDog))] ICat dogCatCat)
{
dogCatDog.Bark();
dogCatCat.Meow();
}
var dogCat = new DogCat();
Foo(dogCat, dogCat); Same idea for locals and fields. // becomes:
void Foo([IntersectionType] (IDog dogCatDog, ICat dogCatCat) dogCat)
{
dogCat.dogCatDog.Bark();
dogCat.dogCatCat.Meow();
}
var dogCat = new DogCat();
Foo((dogCat, dogCat)); Obviously this approach has its own downsides, such as messy signatures. On the upside, languages which don't support this can just pass the same value twice when calling a method like this. |
Beta Was this translation helpful? Give feedback.
-
same #6352 |
Beta Was this translation helpful? Give feedback.
-
Forwarded from #6352, originally in reply to @TahirAhmadov. Already mentioned above is to use generics to handle intersection-typed parameters, but the previous solution wasn't fully performant. The right solution is // Intersection of interfaces.
void Foo<T12>(T12 t12) where T : class, I1, I2
{
}
void Foo<T12>(ref T12 t12) where T : struct, I1, I2
{
}
// Intersection of class and interface.
// T1 is the name of a class.
void Bar<T12>(T12 t12) where T : T1, I2
{
} For a field of type // For holding a reference-typed field. More arities can be added.
public readonly struct Intersection<T1, T2>
where T1 : class
where T2 : class
{
public readonly object Target;
public T1 AsT1
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
get { return Unsafe.As<T1>(Target); }
}
public T2 AsT2
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
get { return Unsafe.As<T2>(Target); }
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private Intersection(object target)
{
Target = target;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static Intersection<T1, T2> Create<T12>(T12 t12) where T12 : class, T1, T2
{
return new Intersection<T1, T2>(t12);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static Intersection<T1, T2> Copy<T12>(T12 t12) where T12 : struct, T1, T2
{
return new Intersection<T1, T2>(t12);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static Intersection<T1, T2> Cast(object t12)
{
return new Intersection<T1, T2>((object)(T2)(object)(T1)t12);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static Intersection<T1, T2> As(object t12)
{
return new Intersection<T1, T2>((((t12 as T1) as object) as T2) as object);
}
} In reply to a comment by @TahirAhmadov
The above implementation is zero-cost when all the annotated optimizations are in force. It is very important to have a safe, in particular, thread-safe, implementation. The above implementation is not known to be safe as far as C# language spec is concerned. However, it is safe for most implementations of C# (and can be made so if it becomes part of C#, so that any non-guarantee of user code does not apply):
In contrast, a safe implementation of discriminated union with a single underlying reference field and |
Beta Was this translation helpful? Give feedback.
-
I've been using unions a lot recently and one of the conclusions I've reached echoes what @JohnGalt1717 said a year ago: ad-hoc unions are an incredibly useful feature and they complement tuples really nicely. Tuples serve a way of defining a collection of disparate values at the point where they are returned from a function: (int v1, float v2, string v3) SomeFunc() => ... And this same "define at the point of use" works nicely with DUs too in a lot of cases, especially when passing values around internally within an assembly: (int | float | string) SomeOtherFunc() => ... Generally I've found that I'm never interested in the container type of such DUs as the returned value is normally immediately handled with some form of pattern matching. I'm not saying there's no need for defining formal union types, especially if the same type is used in multiple places. But these ad-hoc DUs certainly have a place in the language too I feel. |
Beta Was this translation helpful? Give feedback.
-
Here is a real-world example where an intersection of interfaces would be nice: This class constructor takes as parameter an object that must implement Currently, one way to make it explicit would be to make this class generic. But that will lead to all the upstream classes needing to also have that generic type, which is often not feasible. By the way, this could also be done with a feature like shapes (although it would be more verbose). |
Beta Was this translation helpful? Give feedback.
-
Does anyone with knowledge of the JVM and associated languages know how Ceylon and Scala are able to implement union and intersection types without modifications to the runtime? |
Beta Was this translation helpful? Give feedback.
-
A union type can be modelled in C# using tuples: E.g., a union string? f((A? a, B? b) value) =>
value switch
{
_ when value.a is not null => "a",
_ when value.b is not null => "b",
_ => null
}; Compiler can add a syntactic sugar around it, and to let write it a bit simpler e.g., as: string? f(A|B value) =>
value switch
{
A => "a",
B => "b",
_ => null
}; |
Beta Was this translation helpful? Give feedback.
-
What are the thoughts on C# having an augmented design-time type system that doesn't need to be carried over to runtime? I understand that's the primary thing holding back any improvement/modernizing of the type system. As a language it's becoming less and less palatable over time. Over time it's flipped to where .Net is propping C# as a language up, and C# is a detractor from what .Net provides. As TypeScript has grown over the last 10 years, I've found myself enjoying C# (as a language) less and less. It's so rigid, and encourages less type-safe ways of doing things when dealing with more dynamic typings that are handled elegantly & safely in languages like TS. Multiple sources of truth is just gross. |
Beta Was this translation helpful? Give feedback.
-
I first learned about Union and Intersection types from Typescript. What I think is the most important concept I learned about them is-
I like to refer Union type as "Or type" and Intersection type as "And type", for simplicity. In Typescript, just like JavaScript, types are mostly dictionaries of string keys and members of different types as values. So, the union or intersection (based on the keys) of the members from different types is actually possible to generate a new type. So, the concept makes sense in TS/JS world. But in world of C# and .net, I don't think union or intersection of members is a natural idea, even if possible. So, the question is, how the Union/Or types and Intersection/And types should look like in C#?
In my opinion, in C#, union or intersection of types should be defined by inheritance (type hierarchy). Which means, the Union or Intersection of types should produce implicit abstract classes or interfaces instead of concrete types. Then an union of types would mean an implicit base type of the participating types, while an intersection of types would mean an implicit child type of the participant types. That means-
T1
andT2
,T1 | T2
will be defined as an implicit abstract base type ofT1
andT2
. Any object or value of typeT1
orT2
will also be implicitly considered of typeT1 | T2
.I1
andI2
,I1 & I2
will be defined as an implicit abstract child typeI1
andI2
. If a object or value derives from or implements both typeI1
andI2
, then it will be implicitly considered of typeI1 & I2
.As we can see, Intersection/And of types requires multiple inheritance. In .net view multiple inheritance, there can be only a single parent class (optionally) but multiple interfaces. So, following the same rule, we should allow only one single class in an Intersection/And type, while the rest of the participant types should be only interfaces. For Union/Or types, as all classes and interfaces implicitly inherits the
Object
class, no such restriction is needed in my opinion.Syntax:
There can be two types of syntax to define Union and Intersection types- unnamed and named. As I have already used above, the unnamed (inline) definition can use
|
for Union and&
for Intersection.T1 | T2
is Union ofT1
andT2
whileI1 & I2
is Intersection ofI1
andI2
. And the named definition can just be the aliases of unnamed definitions. C# currently only have the file-wide type aliasing withusing
. But programmatical project-wide type aliasing is being discussed in #259 . I would prefer using a new and explicit keyword for aliasing liketypealias
. Then the named definition will look something like-An alrernative for
&
can be+
, which is used in some other languages like Rust-Both #228 and #344 are already discussing intersection of interfaces. #228 suggested to use
union<I1, I2>
(it should beintersect<I1, I2>
IMHO) as unnamed definition, while #344 initially suggested this syntax-While this syntax looks more idiomatic to C#, it cannot be declared inline, which is sometimes nice to have.
Members:
Which from the participating types should become the members of the resultant Union and Intersection types? For Intersection/And types aka union of members, the answer is quite simple, all of them. If there are some actually common member/API? It should not make much difference, unless explicitly implemented by the concrete type. Then the API has to be implemented implicitly or explicitly for all interfaces of Intersection/And type.
For Union/Or type, I think we should go for members of the "most common type" in the inheritance tree. In many cases, it will be just
object
. But if any of the participating type overrides some member of that common type, a call to the member will correspond to the participating type, not the common type. There was a discussion regarding best common type in #33 , but it got stuck in the question, should interfaces be considered. Here the "most common type" does not include interfaces. But if all participating types implements an interface but the "most common type" does not, the members of that particular interface should still be available to the Union/Or type through that interface.In short, we just follow the current rules of C#/.net type hierarchy. And, also there can be extension methods to Union/Or and Intersection/And types.
Usage:
As already mentioned, usage of Intersection/And types (for interfaces) are already being discussed in #228 and #344 . For Union/Or types, there can be following cases-
? :
,??
orswitch
expression combinators-I think, as there is no actual intersection of members in this proposed unions types, they will also act like discriminated unions of some sort (which is being discussed at #75 and #113 )-
Although related, Union and Intersection types can be pursued separately. In my opinion, Union types will be more useful and hence should be looked into at first. What can be the downsides of Union and Intersection types? Let's discuss.
Beta Was this translation helpful? Give feedback.
All reactions