an age is a nonnegative duration where one endpoint is constrained to be a birthday. even birthday could be abstracted. couldn't a bottle of wine have an age?
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
"Programming to an interface" is another great topic for a blog post.
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.
I agree with thinking of exposing an interface, but all data must fit into bytes at some point. It’s best to make it impossible to make it impossible to fuck that up, and also add an interface on top to make it even harder.
An age can be negative if you are modeling a person who hasn’t been born yet. Making these simple things complicated looks like the fragile base class problem again.
> Now we can be sure that the age for any Person will always be within the range [0, 150).
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.
2038 is only 14 years away - and there’s plenty of 32-bit systems still around (including those using 32-bit timestamps). For example, a system I built back in 2011 which controls an Asterisk v11 box is still running, literally unmodified, in production (and it needs restarting by cron job every few days) and I suspect it’s still going to be there in 14 years’ time too.
This insight translates readily into machine learning in the form of "inductive bias". It is rare that I come across a model whose architecture actually bakes in the functional form (i.e. topologies and constraints) of the system being modeled. Forcing models to relearn what should be baked-in domain experience is the most common way to waste energy and training time and bloat your models' parameter count.
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).
Also cultural differences in representing age. South Korea used to be a good example of this, but apparently moved to using birthday to calculate age last year - https://en.wikipedia.org/wiki/East_Asian_age_reckoning
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
I always thought data storage methods where every state is valid were cool - ie. so that I could feed /dev/random into them and it would spit out something. It's even cooler if every valid output has precisely one encoding.
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.
Constrained types would be nice. And you can write a complex thing in most languages, but I think often you just end up with a bunch of Builder code to enforce some invariant. And that's a nightmare. You have to generate it and stuff like that.
I agree with the title, though the age example in the article isn't great.
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.
In Go you rely on convention that if err != nil, the other value is meaningless and should not be used. You have to remember to check this, and some interfaces break this convention and use eg. error+some partial value, or nil+nil or other combination.
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?
More tersely, returning errors is not the problem. Returning errors and results simultaneously (and relying on context to distinguish them) is the problem. Sums are not well modelled by products.
I’m torn on this subject. I’m all for stopping dumb bugs from occurring, But I can’t help but think in the general case this blog post is advocating for putting business logic inside my data layer.
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.
> But I can’t help but think in the general case this blog post is advocating for putting business logic inside my data layer.
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.
The answer here is to have dumb types to store the data in, smarter domain types correct by construction, and only write business logic on the domain types.
Jon Brower Minnoch (638 kg) breaks this program. If a hardcoded constraint is going to go into a program, it's basically necessary to do a bunch of expensive research on what it should be. It's much cheaper to just let the numbers be unless there's a pressing need.
Let me be the first to point out that the article introduces Contract Programming in its extreme form, without calling it by name.
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.
I appreciate the general idea of using strong types, but I think the chosen example was a bit unfortunate and distracts from the actual problem. (Granted, it was a toy example, but still.)
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.
The principle seems reasonable, and is easily applicable for single data elements, but I feel it is a little naive and will break down with collections of data, which is where the real problems usually are anyway.
You just need the right grammar. Design a bunch of operators and operands that do exactly what you want. Make that your working medium. Translate to and from that for working with real world stuff.
53 comments
[ 3.7 ms ] story [ 121 ms ] threadthis 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.
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.
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.
https://en.wikipedia.org/wiki/Cal_(command)#Quirks_(1752)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.
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.
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.
https://en.wikipedia.org/wiki/List_of_postal_codes
It simplifies the job immensely.