← Back to context

Comment by hyperman1

2 years ago

I once got a program that was a never-ending source of bugs. It had huge forms, with fields becoming valid or invalid depending on the state of other fields. Each field had listeners to activate/deactivate controls as needed, but the interactions had become so complex that they were full of tiny mistakes. Each tiny mistake slightly corrupted the state, triggering other mistakes, until the whole thing snowballed out of control. Costly business mistakes followed.

After a while it occurred to me that all state was duplicate/triplicate/..., e.g. a checkbox on screen and a boolean field. Most bugs amounted to inconsistencies between the duplicates. So we wrote for each form 2 big methods: One copied the records to the UI, the other the UI to the record. All listeners became a 3 step process: Copy complete UI to record, do the change only in the record, copy complete record to UI.

In a way this was wasteful: Typing 3 characters would enable or disable most GUI elements 3 times. But computers had become fast enough that this was unnoticeable. The dynamic of the program changed: instead of tiny mistakes in the code spiraling out of control, they would disappear as the next user action would most probably fix them. Bugs basically dried up overnight after what amounted to a small code change.

In this case, it was too late to make invalid states unrepresentable, but we managed to declare 1 part of the state correct, and derive all the other state from it. I learned a lot about state management from that experience.

> but we managed to declare 1 part of the state correct, and derive all the other state from it

Isn't this the essential philosophy of React?

The other approach is INotifyPropertyChanged-style: an event for every change, which is supposed to propagate to all listeners, but only propagate if the value is different. It sounds like this is the one that had failed in the project you were given.

> Typing 3 characters would enable or disable most GUI elements 3 times

I don't think this matters so long as you can coalesce all the redraws. See also "immediate mode" GUIs: if you always redraw everything from the canonical state, which modern computers are extremely fast at, a lot of complexity goes away.

  • We're talking a windows gui application, 20 years past. Compute was not as fast as today, so at the time he speed concern was more relevant.

    In a way, the core change in mindset was realizing that computers got fast enough for this architecture to be reasonable. Some of the devs had worked with a machine having 1MB of memory shared by everyone, and snapping out of that is hard. I remember someone on that project lamenting we would be wasting whole kilobytes of memory. Funny even then, but today I recoil from electron, much for the same reason.

    Today, you'd be absolutely right.

> it was too late to make invalid states unrepresentable

It wasn't, that's exactly what you did, albeit not with types, but by removing redundancies from the state. It's the same reason why normalizing relational databases is a good idea.

  • > It wasn't, that's exactly what you did

    the thing with using types to make unrepresentable state is that it ensures the compiler is the one checking.

    By doing it "manually" this way, you're not saving much effort. You still had to make the analysis of which state is valid, and to code it up (hopefully without a mistake). The state of the program can also temporarily be invalid - just happens to fast for the user to notice (thus "fixing" the bug).

    It's probably the best that could've been done other than a rewrite, but make no mistake - it's not the ideal.

This is the way.

Always separate the truth state from everything derived.

Especially the UI. Ideally have a single "render" method that updates your complete UI from the application state.

Very nice!

This is the essence of model, view, controller, and also one of the very first use cases for it!