This means that there are certain states that may be impossible in practice, but possible to represent in your data. This can lead to unnecessary code at best, or undefined behaviour and bugs at worst.
For example, let's say we want to model loading data from a server.
With ADTs:
type WebData
= NoRequest
| Loading
| Failure HttpError
| Success MyData
With product types only (and enums), how would we reconcile that the request could return an error _or_ data?We could try something along the lines of:
type WebDataStatus
= NoRequest
| Loading
| Failure
| Success
type WebData = (WebDataStatus, Maybe HttpError, Maybe MyData)
Where the error and data fields are optional.But in this case, it would be possible to have both error _and_ data fields populated. Not ideal!
Category theory has a wealth of other concepts which suggest more general non-ADT properties, too. For example (ignore the jargon):
* Finite limits are a generalisation of finite products (i.e. the ADT product type), where we additionally require intersection types.
* Finite colimits are a generalisation of finite sums (i.e. the ADT union type), where we additionally require quotient types - and I'm not aware of any language, other than maybe Lean and friends, which has user-space quotients! Think of trying to define the rational numbers in user-space, but without forcing the user to deal with normalisation.
For instance, (A^B)^C = A^(B*C):
(f) => (b, c) => f(c)(b)
(f) => c => b => f(b, c)
which, of course, is currying and uncurrying.The best I can come up with is that if you want to talk about the product of function types (A -> X) x (B -> X) then a sum type allows you to summarize this a single function type (A+B -> X). With just a product you could already do something similar for the type (X -> A) x (X -> B) which is equivalent to (X -> AxB), but with a sum type you have the dual variant as well.
And I suppose that with sum and product types together you can kind of express BNF like rules directly in the type system. Which makes things like expressing syntax trees a hell of a lot cleaner.
That said ultimately a type system is just a part of the language you can use to express certain rules, you can usually still decide on your own rules you'll just have to enforce them manually.
Relatedly, a single field of an interface type + the ability to cast to a concrete type is very similar to a sum type; Go programmers will use this trick sometimes.
Sum type:
I can be A | B | C.
Interface thype:
I am ILetter. Types A, B, C implement ILetter.
Plausible in memory representation of sum type value: 8 bits for the type tag, plus max-size(A,B,C) bits for type.
Plausible in memory representation of interface type value: 64 bits for the type pointer and 64 bits for a pointer to A, B or C.
(Not as nice as the sum type!)