Invariants

Full discloser, all of this is due to this wonderful article I just read. Dive in a take a read.

So I am sitting at home, doing my normal “zone out and think about types,” and then it hit me; types are pretty cool 🌹. But like, that’s normal now… right? HOLD THE ☎️, I learned a new functional word and wanted to share it with everyone.

Peter Deutsch once quipped that if you get the data structures and their invariants right, most of the code will just kind of write itself. If I had to pick just one nugget of learning from my career in programming so far, that would be it.

…Invariant? I have had heard this word before, smiled and waved my hand and pretended like I understood. But this time, I turned to wikipedia.

An invariant is a condition that can be relied upon to be true during execution of a program, or during some portion of it. It is a logical assertion that is held to always be true during a certain phase of execution.

This was the end of the string that got tugged making a big mess of my brain. My first view of types from days of yore was a way to help the compiler know what was going on and prevent crashes. Describing a class or a function, things that should be readily inferable. Working with typescript, elm, and other functional typed languages, my view has changed.

Types truly shine when they model data.

It could be how our information is stored in a database or reducer, what state our component is in, how we interact with a component, or how we parse information from a server. Types are the language that make assertions about our data which will always be true.

I wanted to pull out one lesson I learned on my current project. I spent far to too little time structuring my data. For example, how I store information in reducers. Here is what the shape of my auth reducer looks like:

type Auth = {
token?: string,
users: {
activeUser?: User,
otherUsers: {
[userId: string]: UserPreview
}
}
}
// Using it in connect
state.users.activeUser && // ...

I have two optionals at two separate locations in my tree. They are inherently linked, but since there is no explicit relationship it is possible (and has happened) to get in invalid states. Much later, I started a side project and modeled my auth differently.

type Auth
= { authed: false }
| { authed: true, user: User, token: string }
type AuthReducer
= { loading: true }
| ({ loading: false} & Auth)
// Using it in connect
!state.loading && state.authed && state.user // ..

Back to the quote, I took a bit longer to think about the states of the application and modeled it via the data structure and types around it. What happened when I did this? It was a huge pain to access the user object. Every time I wanted to use it I needed to check if the app was loaded and if the user was authed. In fact, I found myself disliking writing it so much that I avoided using connect as much as possible and passed down auth state to my components manually. This meant that more components were "dumb" and much easier to reason about.

Holy 💩, huge win. Taking time and encoding these types into reducers forced me to write my components more intelligently. Instead of being lazy and having connected components all over the place, they had strong interfaces via props and their parents were responsible for their well being.

Is this example perfect? Nope. Everything I write is laughable; forget about perfect. Can you do all this without types? Yep. You can make state machines without types! Your functions can enforce your data and things can run smoothly. However, it’s the last bit that is the clincher for me:

It is a logical assertion that is held to always be true during a certain phase of execution.

Types are assertions. The compiler will always enforce them. When types model your data, the compiler ensures that your data is always correct.

Thanks for putting up with me.

❤️