The (arbitrary, 'philosophical') difference between Haskell's `data A = X | Y` and TypeScript's `type A = "X" | "Y"` is that the TypeScript values "X" and "Y" already exist (they're strings). Functions which produce and consume these values already exist: there are certainly many functions which act on `string`, and perhaps there are silly examples which produce such strings too, e.g. a 2D geometry library which uses them to represent axes.
> without having to use wrappers and dealing with the overhead that they bring
I think part of the author's argument is that including such wrappers in our values can sometimes be useful, to convey more information about the domain. As an extreme example, we can represent pretty much everything in number theory using `int`; e.g. the fundamental theorem of arithmetic tells us that `int` is equivalent to `prime[]` (and of course Euclid proved that `prime` is equivalent to `int`), but it's probably more useful to use the second representation, even though it introduces wrappers and overhead.
As a more sensible example, I prefer Haskell's `Maybe` type compared to the "nullable types" offered by some languages, since the latter loses all structure: `Just Nothing` represents a different failure case to `Nothing` (and we can easily collapse them using `join`), whilst `null` doesn't provide that distinction.
And I think that is why the article, while in general touches a very interesting topic, leaves a bad aftertaste for me.
It makes it sound as if one method is better than the other - almost like "union types take away your power, better do it like this". However, both approaches have their place.
> As a more sensible example, I prefer Haskell's `Maybe` type compared to the "nullable types" offered by some languages, since the latter loses all structure
And you just gave a very good example. The two are not the same by any means and I am happy to be able to work in a language that has a Maybe type in the standard library. The classic example is having a map where querying by a key might or might not return a value of type A. Because A can also be null/none, without a Maybe type, this cannot be properly expressed without losing information.
However, and I really want to emphasize this: union types _can_ emulate sum types like Maybe. Simply be creating a wrapper and using it. I.e. creating two unrelated classes/structs Just and None and then defining
struct Just A = ...
struct None = ...
type Maybe A = Just A | None
There is Maybe, emulated by union types.The other way around is not possible though. Using Haskell#s capabilities, it is not possible to emulate Typescript's (or other language's) union types, which I find very sad. They are a useful thing to have. The only thing that might come close is Haskell Liquid and some fancy typelevel programming machinery to emulate union types.
Thank you, that makes sense. My impression was that typescript was chosen as an arbitrary language and that this limitation would be irrelevant for the example, but maybe it was chosen specifically.
I'm mainly a Scala developer, and in Scala 3 we can pretty much do:
case object A
case object B
type X = A | B
or type X = "A" | "B"
Where A and B didn't exist before, but "A" and "B" do. Does that capture the difference that the author wants to express?There is still a difference to Haskell though, in that the "case object A" is its own type and a subtype of X.
Yes, but this is an approach based on thinking not on which values we want to exclude, but rather how to structure our data such that those illegal values aren’t even constructible.
You must have forgotten that you read about this approach in the article.
> type NonEmptyArray<T> = [T, ...T[]];
I'm not very familiar with Typescript, but isn't that also a "constructive" approach, rather than a "constraining" one? It reads to me like "NonEmptyArray is an array that has one element and then more elements that come from pulling elements from another array".
Typescript uses a language construct here for a special usecase, but the point still stands.
If the Haskell method gave you iterability for free I could see the benefit, but it doesn’t.
data EvenList a = EvenNil | EvenCons a a (EvenList a)
deriving (Functor, Foldable, Traversable)
But that isn’t really the point of the example. As the blog post itself points out, it’s likely more useful to use a list of pairs in most situations, and that choice would still be a constructive representation.The example is not about exactly the way you choose to represent the property in question. The example is a demonstration that it’s possible to capture these properties using your type system even if it doesn’t have any first-class support for constraining list types this way. You can choose whichever representation you like the most/is most natural in your language of choice, of course, and you should! The blog post is just pointing out that you shouldn’t give up altogether on being able to capture these properties without changing the type system itself.
The distinction of the two "views" does not make much sense to me. In Haskell when doing `data A = X | Y` also "restricts" `A` to X or Y.
However, as much as I like and appreciate Haskell's typesystem, I find union types very powerful. All the reasons for not have subtyping in Haskell always drill down to problems with type inference or other "practical" issues that seem to be fixable by a more advanced compiler or by accepting some inconvenience - but maybe someone can enlighten me?
Also, being able to do `type X = A | B; type Y = C | D; type Z = X | Y` is very powerful and helps a lot when using code to describe business requirements without having to use wrappers and dealing with the overhead that they bring. Even more so when a typesystem is so powerful that it can express a pattern match on some type `Z` and do: `if(NOT A) ... else if (A) ...` while checking exhaustiveness. I wish more languages would adapt this.