5 ms·
Surface Tension of Software
- apples_oranges 10mo agoGood type definitions are the foundation
- BSTRhino 10mo agoMake your invalid states unrepresentable
- beagle3 10mo agoIndeed. But ... do not confuse your model with reality. There's a folk story - I don't remember where I read it - about a genealogy database that made it impossible to e.g. have someone be both the father and the grandfather of the same person. Which worked well until they had to put in details about a person who had fathered a child with his own daughter - and was thus both the father and the grandfather of that child. (Sad as it might be, it is something that can, in fact, happen in reality, and unfortunately does). While that was probably just database constraints of some sort which could easily be relaxed, and not strictly "unrepresentable" like in the example in the article - it is easy to paint yourself into a corner by making a possible state of the world, which your mental model dims impossible, unrepresentable.
- incognito124 10mo agoThat sounds like this (in)famous stackowerflow question: https://stackoverflow.com/a/6198257 https://stackoverflow.com/a/6198257
- louthy 10mo agoYour example doesn’t validate your point. That’s a valid state made unrepresentable, not an invalid state made unrepresentable. Your example simply demonstrates a poorly architected set of constraints. The critical thing with state and constraints is knowing at what level the constraint should be. This is what trips up most people, especially when designing relational database schemas.
- jacquesm 10mo agoAny assumption made in order to ship a product on time will eventually be found to have been incorrect and will cause 10x the cost that it would have taken to properly design the thing in the first place. The problem is that if you do that proper design you never survive to the stage where have that problem. I think the solution to that is to continuously refactor, and to spell out very clearly what your assumptions are when you are writing the code (which is an excellent use for comments).
- louthy 10mo agoContinuous refactoring is much easier with well constrained data/type schemas. There are fewer edge cases to consider, which means any refactoring or data migration processes are simpler. The trick is to make the schema represent what you need - right now - and no more. Which is the point of the “Make your invalid states unrepresentable” comment.
- watt 10mo agoI do see how it does, in a way. That something the designer thought is "invalid state" turns out a valid and possible state in real world. In terms or UI/UX, it's the uncomfortable long pause before something happens and screen renders (lack of feedback, feeling that system hangs). Or, content flicker when window is resized or dragged. Just because somebody thought "oh, this clearly is invalid state and can be ignored". The real world and user experience requirements have a way of intruding on these underspecified models of how the world "should" be.
- louthy 10mo agoThat’s still a poorly designed system. For UI there should be a ‘view model’ that augments your model, that view model should be able to represent every state your UI can be in, which includes any ‘intermediate’ states. If you don’t do this with a concrete and well constrained model then you’re still doing it, but with arbitrary UI logic, and other ad-hoc state that is much harder to understand and manage. Ultimately you need to make your own pragmatic decisions about where you think that state should be and how it should be managed. But the ad-hoc approach is more open to inconsistencies and therefore bugs.
- ahoka 10mo agoWonder how old is this advice. Must be at least 20 years?
- bpavuk 10mo agothis explains virtually everything: - that's why OOP failed - side effects, software too liquid for its complexity - that's why functional and generic programming are on their rise - good FP implementations are natively immutable, generic programming makes FP practical. - that's why Kotlin and Rust are in position to purge Java and C, philosophically speaking - the only things that remain are technical concerns, such as JetBrains' IDEA lock-in (that's basically the only place where you can do proper Kotlin work) as well Rust's "hostility" to other bare-metal languages, embedded performance, and compiler speed.
- Ygg2 10mo agoHaving tried Kotlin in IDEA, I must admit their refactoring tools for Java are miles ahead of Kotlin. I don't know how strong lock in is.
- bpavuk 10mo agonear-infinite until they finish Kotlin LSP alpha. also, hot take: Kotlin simply does not need this many tools for refactoring, thanks in part to the first-class FP support. in fact, almost every non-Android Kotlin dev I have ever met would be totally happy with analysis and refactoring levels on par with Rust Analyzer. but even with LSP, I would still need IDEA (at least Community) to perform Java -> Kotlin migration and smooth Java interoperability.
- i_am_a_peasant 10mo agowhat is it with this take that oop is dead… even the linux kernel heavily uses OOP. inheritance is what has been changing in scope, where it’s discouraged to base your inheritance structure on your design domain.
- beagle3 10mo agoOOP is different things to different people, see e.g. [0]. Many types of OOP that were popular in the past, are, indeed, dead. Many are still alive. [0] https://paulgraham.com/reesoo.html https://paulgraham.com/reesoo.html
- yuchi 10mo agoWhile the reasoning holds generally, that specific example is wrong. The type the author presents is not the “User Profile” but a “User Profile Load Resource” (or something in those terms). When you actually design interfaces you discover that there are way more states to keep in mind when implementing asynchronous loading. 1. There’s an initial state, where fetching has not happened yet 2. There may be initial cached (stale or not) data 3. Once loaded the data could be revalidated / refreshed So the assumption that you either are loading XOR have data XOR have an error does not hold. You could have data, an error from the last revalidation, and be loading (revalidating).
- mrkeen 10mo ago> that specific example is wrong This will be discovered at compile time if you use immutability & types like the article suggests. > So the assumption that you either are loading XOR have data XOR have an error does not hold. If you design render() to take actual Userdata, not LoadingUserdata, then you simply cannot call render if it's not loaded. The way to produce unspecified behaviour is to enable nulls and mutability. Userdata{..} changing from unloaded to loaded means your render() is now dynamically typed and does indeed hit way more states than anticipated.
- jstimpfle 10mo agoYup, it's quite rare that ADTs (or Rust enums) are so clear cut and obvious. The idea that the data model looks like enum XYZ { case A(B, C, D, E); case F(G, H, I, J, K); case L(); case M(N, O); } is just not true in practice. I think messaging is one case where it can happen, but even there it's often good to combine fields and share them (and common code) over multiple types of messages. If Messages A and B both have a field "creationTime" or whatever (with identical semantics), it's probably a bad idea to model them as separate fields, because that leads to code duplication, which is unmaintainable. Maybe I can be more precise by proclaiming that ADTs can be good to be clear what's "there", so they can be used to "send" information. But to write any useful usage code, typically a different representation that folds common things into common places is needed. And it might just happen that field F is valid in cases A and B but not C. That's life! Reality is not a tree but a graph. That's why it's a bad idea to try and model the exact set of possible states and rule out everything else completely in the type system. Even the most complicated type systems can only deal with the simple cases, and will fail and make a horrible mess at the more complicated ones. So I'm saying, there is value to preventing some misuse and preventing invalid states, but it comes at a cost that one has to understand. As so often, it's all about moderation. One should avoid fancy type system things in general because those create dependency chains throughout the codebase. Data, and consequently data types (including function signatures) is visible at the intersection of modules, so that's why it's so easy to create unmaintainable messes by relying on type systems too much. When it's possible to make a usable API with simple types (almost always), that's what you should do.
- ram_shares 10mo ago[dead]
- ttoinou 10mo agoWhat does it mean when loading is false, error is Some, and data is also Some? The type allows nonsense. You write defensive checks everywhere. Every render must guess which combination is real How about you never write the wrong state in the first place ? Then you have nothing to take care of
- wvbdmp 10mo agoThen you have to take care of nobody ever writing the wrong state, which is increasingly annoying the more people work on the thing.
- qezz 10mo ago> How about you never write the wrong state in the first place ? Indeed, and tagged unions (enums in Rust) explicitly allow you to avoid creating invalid state.
- JSR_FDED 10mo agoThat’s why I aim for my code to be custard-like.
- humanfromearth9 10mo agoNice story. Now, is that gonna help design better software in any particular way?
- codeflo 10mo agoOnce you notice the pattern, you see it everywhere: > Stability isn’t declared; it emerges from the sum of small, consistent forces. > These aren’t academic exercises — they’re physics that prevent the impossible. > You don’t defend against the impossible. You design a world where the impossible has no syntax. > They don’t restrain motion; they guide it. I don't just ignore this article. I flag it.
- htk 10mo agoPlease elaborate. I thought the article was interesting and would love a contrasting take. Edit: thank you for the answers, I don't know how I missed that em dash clue.
- jiggawatts 10mo agoIt is a sign of ChatGPT's style.
- polytely 10mo agoit has a very LLM style of writing
- YJfcboaDaJRDw 10mo ago[dead]
- Jordan-117 10mo agoNot just the em dash, but the pervasive "not X, but Y" construction. (oh no am i one of them?)
- iamcalledrob 10mo agoIn theory, I think the article is correct. Yet in practice, I've found that it's easy to over-model things that just don't matter. The number of potential states can balloon until you have very complex types to handle states that you know won't really occur. And besides, even a well modelled set of types isn't going to save you from a number that's out of the expected range, for example. I think Golang is a good counter example here -- it's highly productive, makes it easy to write very reliable code, yet makes it near impossible to do what the author is suggesting. Properly considered error handling (which Go encourages) is much more important.
- frizlab 10mo agoThat’s one of the reasons why I love Swift. Representing and using a state is easy and super readable. It is an incredibly good language.