Remix.run Logo
phkahler 3 days ago

Sum type sounds like the old Variant from VB.

We use something similar in Solvespace. Where one might have used polymorphism, there is one class with a type enum and a bunch of members that can be used for different things depending on the type. Definitely some disadvantages (what does valA hold in this type?) but also some advantages from the consistency.

WorldMaker 2 days ago | parent [-]

VB's Variant is a bit "assume I know what this is" dynamic typing, almost Python-like duck typing. It sounds like your solution is also more akin to duck typing.

Sum Types are still themselves Types. The Sum Type itself like `int | string` is a Type. That Type generally only provides the intersection of methods/abilities, which depending on the language might only be a `toString()` method and the `+` operator (such as if the language uses that operator for string concatenation and also supports implicit conversions between int and string). The only safe things to do are the methods both types in the Sum support, so that intersection is an important tool to a type system with good Sum Types support.

But the other part of it is that Sum Types are still made up of types with their own unique shapes. When you pattern match a Sum Type you "narrow" it to a specific type. You can narrow an `int | string` to an `int` and then subtract something from it or multiply something with it. You can narrow it to a `string` and do things only strings can do like `.split(',')`. Both types are still "preserved" as unique types inside the Sum Type. A good compiler tests your type narrowing in the same way that a good OOP compiler will typecheck most of your casts to higher or lower types in an inheritance tree.

Given those cast checks, it's often more the case that you want to implement Sum Types as inheritance trees with casts, because the compiler will be doing more of the safety work. You write a base-class, probably abstract, to represent the Sum with only the intersection methods/operator overloads, then leaf classes, probably sealed/final, for each possible interior type the Sum can be, and use type checks to narrow to the leaf classes or cast back up to the SumType as necessary.

That's actually a lot of work in most OOP languages to build. The thing about Sum Types in languages that support them is that a lot of that from pattern matching/narrowing, to computing the intersection of two types, is free/cheap/easy from those compilers. The compiler understands `int | string` directly, rather than needing to build by hand a class IntOrString with sub-types IntOrStringInt and IntOrStringString. (It start to get out of hand when you start adding more types to the sum. IntOrStringOrBoolIntOrString for narrowing from `int | string | bool` down to `int | string`, for instance, which in OOP maybe can't be the same type as IntOrString alone depending on how you want to narrow/expand your sum types.)

(Also, not to get too much into the weeds but building a class structure like that is also more like a specialization of Sum Types called Discriminated Unions because each possibility in the Sum Type has a special "name" that can be used to discriminate one from the other. A lot of people that want Sum Types really just want Discriminated Unions, but you can build Discriminated Unions easily inside a Sum Type system without native support for it, but you can't as easily build richer Sum Types with just Discriminated Unions. In Typescript you will see a lot of Discriminated Unions built with a `type` field of some kind, often a string but sometimes an enum or symbol, which would also resemble your solution, but again the key difference is all the types that are summed together may have entirely different shapes and Typescript supports the sum type intersection calculations automatically, so maybe the top level sum just has the `type` field itself and nothing else, but you can use that `type` field to narrow to very specific shapes.)