It compiles. It runs. So what's wrong?
Let's start with a puzzle.
void resetPassword(String email) {
// Logic for sending reset password email
}
What is the problem with this method?
Answer
It might be used like so
resetPassword(user.getPassword());
You might be thinking I've gone off the rails for claiming there is problem in piece of code that has no logic in it, but there is a bug waiting.
If you've spotted the problem you sure know that it will be very embarrassing if you need to fix such a bug.
Let's have another.
record User(String name, String email, String address) { }
What is wrong this time?
Answer
When instantiating, arguments can be swapped around.
new User("todoniko", "Sofia, Bulgaria", "drama@mail.com");
It compiles. It runs. No warnings. No type errors. But the potential for bug is there.
Actually it is a whole class of bugs that the compiler could prevent - but doesn't, because we never told it how.
Addressing the root cause
As with many things in programming we have jargon for this - it is a code smell it is primitive obsession.
We can encode way too many things with a String
- password, username, email, file path etc. But all those instances mean different things and communicate distinct purpose. In our heads we infer the meaning from the names and expose the encoding for everyone to (ab)use. We have failed at abstraction.
Let's remedy that by wrapping/hiding the encoding/primitive type and expose only the meaning.
record Email(String value) {}
record UserName(String value) {}
record Address(String value) {}
record User(UserName name, Email email, Address address) { }
void resetPassword(Email email) {
// Logic for sending reset email
}
What have we achieved?
Now the potential for bugs from passing the wrong argument has come down significantly. When our types are clear, bugs become rare.
Now our types communicate meaning instead of encoding and this is equally useful for both humans and AI agents.
Conclusion
The problem and solution in this article are not novel in any way. Actually they are too old to keep pretending that they do not exist.
I have drawn inspiration from several sources and if you are daring enough to step out of the mainstream languages we use today you can have a look at:
- Designing with types: Single case union types (F#)
- Haskell mini-patterns handbook (Haskell)
- Type blindness (Elm)
Please, don't be primitive.
What is next
In the next article we will upgrade our wrapper type so that we are not going to care if it needs validation. If it exists it is valid. The instance of the type will be the only evidence we need. We will achieve correctness-by-construction with opaque types.
Top comments (0)