Make Invalid States Unrepresentable
awwsmm.com
awwsmm.com
To be pedantic, "date of birth" does not actually imply "age". At the very least you need "instant of birth" instead, since dates are subject to calendars and time zones. Also Feb 30th happened in 1712 in Sweden; beware of modelling a domain so tightly that you leave some of it unmodelled (cough email-address validation regexes).
In many cases the only feasible way to model the domain with types is to cut the scope as much as possible.
https://www.cs.rice.edu/~javaplt/411/23-spring/NewReadings/f...
September 1752 was even stranger, with 11 missing days:
> The Gregorian calendar reform was adopted by the Kingdom of Great Britain, including its possessions in North America (later to become eastern USA and Canada), in September 1752. As a result, the September 1752 cal shows the adjusted days missing. This month was the official (British) adoption of the Gregorian calendar from the previously used Julian calendar. This has been documented in the man pages for Sun Solaris as follows. "An unusual calendar is printed for September 1752. That is the month when 11 days were skipped to make up for lack of leap year adjustments.
$ cal 9 1752
September 1752
S M Tu W Th F S
1 2 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
https://en.wikipedia.org/wiki/Cal_(command)#Quirks_(1752)One of the things I see often in code reviews is the wrong use of product types where sum type would be better. Things like returning error and value separately and the value is meaningless/undefined if there was an error
This is much more frequent in languages which make use of sum types hard (looking at you std:: variant) compared to ones that make it easy (like Rust). Of course Go is particularly terrible at it and stubborn in it's design, one of the main reasons I hate using it when forced to.
If there was an error, the value should be meaningless. I don't understand what your complaint is against returning both the value and the error.
(Python dev learning Golang, I like the "return error" idea)
With Rust's Result<T> you either get the value, or the error. Never both or neither. Those states are simply unrepresentable. absl::StatusOr does the same in C++, with slightly worse ergonomics.
> If there was an error, the value should be meaningless. I don't understand what your complaint is against returning both the value and the error.
Think of it this way: why are you even returning a meaningless value in the first place?
What TFA says: yeah, just make invalid states unrepresentable in the type system.
I am reminded of blog post I read some number of years ago that represented Pokémon type valances in his programs type system. Ironically, in the blog post itself there was a table showing how all the types interacted.
If your data has complicated restrictions on what is valid, I feel it’s much better to serialize those restrictions in some database/config, as opposed to mucking up the physical structure of your program. That way when the rules inevitably change, you just have to change your config instead of rework your type system or database relations.
That’s not a bad thing, provided the business/domain rules are invariant.
> That way when the rules inevitably change, you just have to change your config instead of rework your type system or database relations.
This is why it makes sense to do fast-moving projects in, say, TypeScript on NodeJS (or back-in-the-day: PHP) with a denormalized object-store (e.g. JSON blobs in Redis) and lots of E2E tests - while doing legacy-business stuff in, say, .NET and RDBMS.
It’s disappointing how often people instead misinterpret the above to stereotype a platform as good or bad (well, PHP probably deserved it…). Right-tool-for-the-right-job and all.
Integration complexities can be ignored often since they are typically longer term considerations.
Hopefully, in the future, we'll look at this like we look at two-digit year storage or 32-bit seconds-since-the-epoch storage, and the Y2k or Y2038 problems.
Obviously you could store birth year, but that leads to other issues.
Huffman or arithmetic encoded data is one example of this. And formats that build off that (eg. most video formats) can decode random data as long as you put the right headers on, sometimes making beautiful artwork.
case February => assert(value >= 1 && value <= 28)
Last time I checked, 2024 is a leap year, so Feb 29 is valid…This is something that Dlang supported as a compiler feature for ages now: the ability to make an assertion about the object's internal state and have the compiler validate it whenever it changes.
Having it supported by the compiler rather than kludging the same out of a type system neatly sidesteps the problems named, like the validity of one field depending on the value of another field: you'll just use an invariant on the level where it can be validated, rather than on the basic types, and you'll also be able to use plain assertions rather than type constraints.
Any system dealing with people will inevitably end up dealing with someone whose age is unknown. If the system cannot process this, these (very real) people will become "unrepresentable", with all kinds of consequences. Imagine a hypothetical story like "The hotel canceled my reservation because their system didn't have my age." Sounds familiar?
Also, ZIP code should be string, and you had better not have a type constraint for it, unless you never plan to sell anything overseas.
this enables the query you care about ageof(person, onday)
i like to think about types, not as isomorphic to some tuple but as the package of methods they support
interfaces are more important than classes, is the pithy exposition
But yes, you're correct that there are many different ways to model this. Which one is best for your particular use case depends on the details of the problem you're trying to solve.
It simplifies the job immensely.