Snipped and backdated from a previous Reddit comment of mine.
Implementing union and intersection types could allow for interesting things. I know that at least Typescript and Ceylon implement them.
One major annoyance this alleviates is strongly typed collections with mixed types.
Now, this shouldn’t be confused with regular algebraic data types (ADTs) a la Haskell or F#’s “discriminated unions”, for example. ADTs kind of solve the problem but in a less powerful way. You have to define a new ADT type upfront and you can’t “mix” ADTs togeather. You only can compose them.
For example, if I wanted to define a list that contains either an int
, string
, or “nothing” in F#, I would do:
type Option<'a> =
| Some of 'a
| None
type Element =
| EInt of int
| EString of string
let xs: list<Option<Element>> = [Some(EInt 1); Some(EString "hello"); None]
In F#, the type inferencer would infer that the type of xs
without me writing the type out, but I had to still specify a new ADT upfront. And I can’t properly mix the types in the Option
ADT with the types in Element
ADT and have to wrap around the other.
Instead, in a type system with union and intersection types, you could make a union of the different types you want like:
let xs: list<int | string | null> = [1; "hello"; null]
And ideally, you would have type inference so you could have it still be statically typed without having to type out all possibilities:
let xs = [1; "hello world"; null];
You would then need to narrow the type down in order to safely use a value. This could be accomplished by using either pattern matching or by using control flow analysis “as a language construct” (Typescript has “type guards”, Kotlin has “smart casts”).
The funny similarity is that both do the same thing, but the former is usually implemented in a functional-style language while the latter in an imperative-style language.
Of course, this is just one feature and I haven’t even bothered discussing intersection types…