In my experience with languages that lack concise sum types and pattern matching, you end up with data types that have lots of implicit invariants. If you find yourself writing docs or thinking in your head things like "Field X is only set if field Y is true" or "If field X is non-null then field Y must be null and vice-versa", then these are indications that sum types would model the data better. Then these invariants can be followed by construction, and it becomes clear what's available at data access - great for interface clarity, robustness, dev tools, morale, etc.
Relatedly, storing booleans is a smell, imho typically an enum or sum type is always better in languages that have concise syntax for these. True and False are meaningless without context, and so can easily lead to errors where they are provided to a different context or generally misused due to a misunderstanding about the meaning.
> if your type-system is sufficently strong to express this
No fanciness needed, just plain old sum types. It is certainly possible to express those invariants directly in languages with a dependent type systems or refinement types like in liquid haskell - see https://ucsd-progsys.github.io/liquidhaskell-tutorial/Tutori.... It's typically much easier to reason about and use sum types, though.
Of course these examples are trivial and silly, but I see instances of these patterns all the time in big co software, and of course usually the invariants are far more complex but many could be expressed via sum types. I've seen loads of bugs from constructing data that invalidates assumptions made elsewhere that could have been prevented by sum types, as well as lots of confusion among engineers about which states some data can have.
> Field X is only set if field Y is true
Original gnarly C style pattern:
struct TurboEncabulatorConfig {
// When true, the turbo-encabulator must reticulate splines, and 'splines'
// must be non-null. When false, 'splines' must be null.
bool reticulate_splines;
struct Splines *splines;
};
> If field X is non-null then field Y must be null and vice-versa.
Original gnarly C style pattern:
struct TurboEncabulatorConfig {
// When non-null, lunar_waneshaft must be null.
struct Fan *pentametric_fan;
// When non-null, pentametric_fan must be null.
struct Shaft *lunar_waneshaft;
};
Type systems are of course not the only possible mechanism that can enforce these kinds of invariants. In dynamic languages, schema style solutions (eg malli or spec in Clojure) have advantages: they have more expressivity than typical type systems, you can manipulate them as data, you can use in contexts that are not statically verifiable, like at the data interfaces of yuor app.
I agree it has more flexibility, but more expressiveness is a double edged sword. Type systems are often not Turing-complete, or at least they are limited in some ways. A language being very expressive means it cannot be run at compile time, sine we cannot know if it terminates.
If we cannot run it at runtime now we need tests to hit that path or to test it manually to actually know if it works.
Relatedly, storing booleans is a smell, imho typically an enum or sum type is always better in languages that have concise syntax for these. True and False are meaningless without context, and so can easily lead to errors where they are provided to a different context or generally misused due to a misunderstanding about the meaning.