Skip to content

Instantly share code, notes, and snippets.

@sistemd
Created April 9, 2022 13:30
Show Gist options
  • Save sistemd/7134e1f8802067aacc01f6d819d21d96 to your computer and use it in GitHub Desktop.
Save sistemd/7134e1f8802067aacc01f6d819d21d96 to your computer and use it in GitHub Desktop.

There are two goals we often see in programming that I consider almost synonymous: writing readable, clean, maintainable code, and writing correct code. This is because unmaintainable and hard-to-understand code leads to bugs: even if the original author didn't leave any bugs, eventually a maintainer will misunderstand the code and introduce a bug. And in this situation the original author is equally to blame for the bug as the maintainer.

Type systems are the single most powerful weapon for writing readable, clean, maintainable code. This is the principle of abstraction: types are what gives meaning to bytes. If all programmers had room-sized brains, they wouldn't need types, because they'd be able to hold all details about all bytes in their head at once.

I, for one, make a point of forgetting code as soon as I write it: if I need to keep context in my head all the time, then I'm not doing a good job at writing code. The trouble with me is that I also make a point of forgetting code as soon as I read it, too. (The upside of this is that I keep my head free of information overload.) (@britttttk this is why I seemed a bit lost in the settlement system, despite having worked with it for months: I forget these things immediately and often intentionally.)

Dynamic type systems serve the purpose of interpreting bytes well enough. But there is an additional property of static type systems: because types are generally way easier to describe than implementation details, static type systems have an additional property of being very lightweight proof systems, further helping developers avoid bugs.

To illustrate what I mean by "types are generally way easier to describe than implementation details". Let's say I'm implementing a function for hashing the inverse of a string. The implementation of such a function can be extremely complex, especially if I'm writing it from scratch. But I can immediately say: this function needs to accept a string and return a byte array. The types are trivial, and yet they give a very useful contract: that function cannot accept anything other than a string, and it cannot return anything other than a byte array. So I can prove that a whole class of bugs simply cannot happen. (So long as the type system is safe, which is a topic for another rant 😄.)

However. There is yet another property of static type systems, that is to me even more important. In fact, this is the most important element of modern software development. Type systems serve as documentation in code.

Most commonly, documentation is written in the form of comments. Doc comments are good: I think they're important and they help people understand your code. But there is one big problem with comments: they are not code. So while code can say one thing, the comment that's supposed to describe it can say something completely different. So the comment now becomes not only useless, but even worse it's actively harmful and serves to further confuse the reader. That's not good.

Type systems are the way to write self-documenting code: they serve as documentation, but unlike comments, they are code. So they present unbreachable contracts: within a sound static type system, it's impossible for a variable's type signature to say string and for the variable to hold an int. Such code is refused by the compiler: it simply cannot happen.

I suppose that the difference between a string and an int is obvious enough. But with comments, you can describe way more than if something is simply a string or an int. And I agree: type systems can never entirely replace comments. However, I believe they can get way further in that direction than most people think.

The ILightningNode, UnfundedPsbt, and FundedPsbt types are a perfect example of this. I could have simply had a Psbt type, and with a few comments, I could have explained that the FundPsbt method must be called with an unfunded PSBT, while PublishPsbt and ReleasePsbt must be called with a funded PSBT. However, I made such comments completely unnecessary by encoding these facts in code, as part of the type system. Due to constructor contracts, an UnfundedPsbt cannot hold a funded psbt, and a FundedPsbt cannot hold an unfunded psbt. And the relevant part of ILightningNode's interface is now entirely self-documenting.

There is one final benefit of static types, which is often brought up by the functional programming people: when you see a function's type signature, it helps you make reasonable assumptions about the function's implementation, i.e., what the function might do. For example, if you know the problem domain, and you see a function that takes an UnfundedPsbt and returns a FundedPsbt, you can make a reasonable assumption about what that function does: it funds the psbt. Same for a function that takes a FundedPsbt and returns an OnchainTransaction: this function probably broadcasts the PSBT to the Bitcoin network. When paired with function names, type signatures leave little uncertainty as to what the implementation of the function might be: just like the name, they serve as further documentation of a function's behavior, allowing you to understand the function without having to read its implementation in detail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment