beeminder / blog

3 stars 2 forks source link

A Story About How Bad If-Statements Are #225

Open dreeves opened 1 year ago

dreeves commented 1 year ago
### Desiderata
- [ ] Have someone read a coherent draft
- [ ] Preview link
- [ ] Keywords/tags
- [ ] Title Image
- [ ] Link preview excerpt
- [ ] Publish
- [ ] Blog blurbs
- [ ] Tweet
- [ ] Tip of the day

(from a daily beemail)

We learned the hard way that when creating a new Beeminder goal, the defaults have to get you on the hook immediately. We used to start all new goals with a week of safety buffer by default. Which makes sense because you can't change anything within the akrasia horizon, and if you messed something up during setup (like picking an unreasonable rate) then you'd be stuck with it for a while.

It makes sense but turned out to be bad. We need to harness the immediate excitement implied by bothering to set up the goal in the first place. We don't want to waste that initial week for the user, and we want to hook the user while they're hot, when they'll be more motivated to invest the energy to figure all this stuff out. If a user creates a goal and then forgets all about it for a week, that tends to not end well.

But what to do about users who were baffled or galled that Beeminder was expecting them to do something tomorrow when they explicitly said they were committing to such-and-such per week (or per month or whatever)?

For quite a while we thought the solution was going to be to change the default amount of safety buffer to match what the typical user would expect. If you chose "2 per week" then we figured a user would expect to be able to go 3.5 days or maybe a full week before they'd have to do anything. So we figured we could default to that much initial safety buffer (they could still explicitly choose a different amount) and then the user would not be so surprised.

Spoiler: Focusing on minimizing user surprise is not the right thing to do.

We just didn't know how else to avoid the common confusion of people's first beemergency being sooner than they expected. We even spent a silly amount of time describing formulas mapping chosen rate and rate units to default amount of initial safety buffer. Eventually we solved the immediate problem by forcing the user to convert their rate to a daily rate, though that's not great either.

I think this is a nice illustration of how our Anti-Magic Principle -- blog.beeminder.com/magic -- is better than the Principle of Least Astonishment (POLA). Trying to minimize the surprise users were experiencing really led us astray. When there's one case where the right thing is happening (user creates a daily goal and wants to be on the hook right away) and another case where the wrong thing is happening (user creates a weekly goal and doesn't expect to have to do anything for the first week) it's tempting to add an if-statement to make the right thing happen in each case. But our hard-won anti-magic lesson is that if-statements are bad. We did better (though not yet best) to hack off a branch of that if-statement altogether and force everyone into the case where the right thing happens. Namely, you just have to specify your goal with a daily rate.

Again, not the ideal solution but a huge step up from trying to solve the problem with new layers of duct tape.

PS: The ideal solution is waiting on some infrastructure work we're doing to make it easier to have more dynamicness in our UI. Like you should be able to specify your commitment how you like and the UI just very clearly, in real time, shows both what daily rate that implies and exactly when your first beemergency will be.

PPS: Another instance of "if-statements are bad" be helpful on 2023-04-07:

  1. i give a beedroid bug report to adam involving a regression caused by android being awful.
  2. he's like, i could patch that but it'd be an if-statement.
  3. i'm like, good call, lemme think, ok, what about doing such-and-such.
  4. adam's like, perfect, yes, 2-minute fix.

Obviously the new solution was better regardless but I think the if-statement aversion is helpful in nudging us toward those better solutions.

Transcript in case it's an interesting case study

DR in the #beedroid channel: Replicata: 1. tap widget that has a default value set, call the value X 2. tap submit 3. datapoint of X is submitted 4. value field is cleared out Expectata: Previously the value field kept the value of X set so I could hit submit multiple times in a row for sending multiple +X's to the graph. Resultata: But now in dev3 I can't do that. Workaround: do the swipey thing to actually exit the beedroid app and tap the widget again. Then the X is pre-filled again. AW: do you know when you did that last and had it work how you wanted it? DR: based on datapoint timestamps, it worked per expectata around 11pm on Apr 5 AW: there weren't any changes to anything in DataEntry in dev3, maybe not in any of the alphas. it's possible that its related to immediately submitting the datapoint rather than scheduling a sync. this is utterly horrific. yeah, that's what it is. it doesn't look like it has anything to do with default values. do you want me to add an if so that it erases the value field on successful completion only if it didn't come from a default value widget thing? (edited) DR: so gross but, yeah, sounds like the alternative is spending more time than it's worth rethinking everything :( or (and cut me off if i'm still thinking about this in 5 minutes) could it be like: 1. always erase the value on successful completion, that's a useful indicator that the submission happened. 2. always fill in the X when the widget is tapped. (AW: i think that's what it does now. DR: no, see workaround in resultata above. it only works if i cause the app to actually exit.) DR: so i can no longer hit submit multiple times in a row but that was a little weird anyway. at least i can pretty quickly tap the widget, hit submit, jump back to home screen, tap widget, hit submit again, etc. AW: oh, hmm. task affinity. that may be a 2 minute fix.

Tangentially related thing: Boolean Blindness

ifs-are-bad thing is for the design stage, not implementation but zzq pointed me to this delightful thing for haskell programmers: > "This [boolean] being true means that e and e’ are equal, whereas this other [boolean] being false means that some other two expressions are not equal." Keeping track of this information (or attempting to recover it using any number of program analysis techniques) is notoriously difficult. The only thing you can do with a bit is to branch on it, and pretty soon **you're lost in a thicket of if-then-else's**, and you lose track of what's what. Evolve the program a little and you're soon out to sea, and find yourself in need of SAT solvers to figure out what the hell is going on. from https://sboosali.github.io/mirror/boolean-blindness.html For reasons expanded on in that article, zzq says he'd prefer "booleans are bad" to "ifs are bad".

Cognata

Verbata: if-statements are bad, anti-magic, yagni, software engineering principles,

dreeves commented 1 year ago

Me in Slack today, when Adam mentioned scales that try to factor in past readings to smooth out your weight graph and Nicky mentioned their dad weighing the Christmas turkey by standing on a scale while holding and while not holding the turkey:

gah, that is a beautiful example of why violating anti-magic is bad and wrong! how else are people supposed to weigh xmas turkeys??

(i really am proud of the anti-magic principle. maybe mostly because of how stupid past-me was with auto-widening roads and odometer resets and ... god there's an embarrassingly long list of anti-magic violations from the early years of beeminder. but even now it continues to help me. i'm perpetually tempted to make things "smarter" and then i think "ok but juuuuuust suppose i wanted to be dogmatic about anti-magic" and so often that points me at a better design that doesn't need the bit of "smartness" i had been tempted to add. like i still don't know how helpful it is to anyone else but it really helps me.)

(and, actually, i guess i don't think it's just me, though possibly my formulation of it is too specific to me. like the programmers of the scale adam tried to weigh an 8 pound thing on via taring were like "what about the use case of someone weighing in multiple times in a row? we can improve the accuracy by taking a mean to average out the noise! let's see, if time_delta less than, say, 100s then ..." and alarm bells need to be going off in their heads. no if-statements! no arbitrary thresholds! no magic!)

dreeves commented 1 year ago

PS: Adam's make a cogent argument that there may be a better heuristic that subsumes this one. Maybe something about minimizing state and maximizing simplicity and transparency.

Also he points out "ifs are bad" sounds all wrong but maybe "extra ifs are bad" could at least point in the right direction. I don't understand his point yet though. Or we're on different pages in some way. My "ifs are bad" means "avoid them" or "when the business logic branches, consider if you really need both branches" which seems to already imply "extra ifs are bad" but Adam pointing that out suggests that... something... still thinking.

dreeves commented 1 year ago

another example the other day, mary reporting a bug with some click-to-toggles not working on the dashboard:

doh, no, i was so confused by this until bee reminded me that it's just kyoom goals that don't let you toggle them. perfect example of why the anti-magic principle is so important!

dreeves commented 1 year ago

new discussion in the community discord that feels like real progress in how i'm thinking about this:

transcript copypasta

HIVE: there's a blog post i want to write about how bad if-statements are -- kind of a follow-on to the anti-magic principle. an example from last week is someone reporting buggy behavior with toggling absolute vs delta amounts due on the dashboard. some of them you click and they toggle, others don't. even i was baffled by this until i remembered the backstory: at some point we had the bright idea that it's dumb to see absolute amount due on a cumulative goal where you only ever think about the deltas, like the +1's you add each time you do a thing. seeing that you need your total pushups to hit 234,712 by midnight is never helpful. which is all fine but we implemented it by sticking in an if-statement: if !kyoom then allow toggle. that should've been a red flag. i think adam and zzq (?) would say it's not the if-statement that's the red flag there. but for me this "if-statements are bad" heuristic seems to keep being exactly the nudge i need towards better design decisions. hence my inclination to blog it -- it's something that would've helped past-me a lot. but i guess i could use advice on being less ... irritating about it? or less infuriatingly wrong seeming to those less like me? or maybe i can find a beautiful generalization of this heuristic that everyone agrees on? THEO: The Zen of python says: “special cases aren't special enough to break the rules.” APIARY: random tip of the day: https://en.wikipedia.org/wiki/Zero_one_infinity_rule i kind of think of it as "gravitate to the extremes". like avoiding arbitrary thresholds or settings. [Zero one infinity rule](https://en.wikipedia.org/wiki/Zero_one_infinity_rule) The Zero one infinity (ZOI) rule is a rule of thumb in software design proposed by early computing pioneer Willem van der Poel. It argues that arbitrary limits on the number of instances of a particular type of data or structure should not be allowed. Instead, an entity should either be forbidden entirely, only one should be allowed, or any numb... THEO: Related to your comments in ⁠hive, if you have n>2 of something, having an if statement which covers precisely one of those cases is often a sign of an incorrect data model. (Having a set of such statements, or a switch, such that all cases are covered, is usually fine) 🤔 that does sound true. so maybe... one if-statement is bad code smell but a comprehensive if-else-else-else suggests irreducible complexity? originally saying "if statements considered harmful" was completely tongue-in-cheek, like obviously they're fundamental to all programming at every level and it's not at all like the "goto considered harmful" it alludes to. (i was amused by the maximally provocative version and maybe i accidentally sounded like a crazed zealot at some point? anyway, we can stick to the object-level here.) THEO: I mean, the functional programmers would be confused about these “statements” you talk of ha, yeah. i think i mean "when there's a branch in the business logic, ask yourself if branching is really necessary" or when articulating a design, when you hear yourself say "if foo then we'll do X" always ask "what if we just always, or never, do X?" minimize how many parallel world states you have to reason about? i fear that when we get it hammered into a shape that's sufficiently unobjectionable that it will no longer be useful. (there's also the problem that even the most profound insightful wisdom eventually feels obvious once you've internalized it enough) THEO: It sounds like you are saying it would be bad to add ifs to your statement about how ifs are bad. maybe! i want a guiding principle, something that makes me notice when a design is going awry, a mantra that exerts pressure in the right direction. "if statements considered harmful" is ... silly but maybe achieves that? ("minimize parallel worlds" also sounds kinda cool and possibly would be less irritating though) THEO: You might find this article interesting: https://byorgey.wordpress.com/2009/01/12/abstraction-intuition-and-the-monad-tutorial-fallacy/ oh, wow, so good. brent yorgey is amazing. i could totally be suffering from the Monad Tutorial Fallacy here 🤔 THEO: It doesn’t mean there’s not value in eg a pithy self reminder though i am wanting it to be more than that though. i see it all the time, people reaching immediately for conditionals to solve design problems. "i think the system should show X if your Y is above Z" and it feels to me like there's massive value in having a way to shut that down. and the word "if" in those wrongheaded design ideas feels like an apt trigger (the "above Z" is a more specific trigger, in the specificity-vs-sensitivity sense. if you have an arbitrary threshold like that, that's a strong indicator of bad design. "do X if [any conditional]" is a more sensitive trigger -- it'll yield lots of false positives. what i find valuable is having that trigger fire and explicitly thinking about whether it's a false positive or not.) RPERCE: I think if statements being mostly bad is an interesting thing to play with. I typically like a few ifs early on in a function definition, like if (!account_id) { return Collection.empty() }) to kind of check assumptions but then, thinking about it, I do try to avoid ifs. I like the "minimize parallel worlds" framing. maybe it's not "mostly bad" but like "20% chance of indicating a bad design decision". so mostly good but bad often enough that it's worth noticing and checking if you're in a 20% case. (now of course i have to reformulate this heuristic to not have that hideous, arbitrary "20%" in it! (i'm mostly er fractionally joking -- totally possible that the Right Formulation has a number in it)) RPERCE: yea, maybe s/mostly/often enough to squint at/ RPERCE: As a concrete example at work, I gave some feedback about a design recently that was looking to add a new type of Widget that could hold multiple Thingums, instead of the currently implicit 1:1 mapping between Widget and Thingum. I suggested instead treating all Widgets as if they were fundamentally multi-Thingum... but that most happened to have a Thingum list of length exactly one, and that way all the fancy Thingum handling falls out for free. ah, beautiful example of design wisdom. there are surely multiple ways to spot the hideousness of the new multi-thingum widget type but i contend that "if statements are bad" is a reasonably elegant and reliable way to spot it. but, yeah, it's an elegant generalization to something you actually need -- it passes the YAGNI check RPERCE: and, an important bit that I left out, that's exactly how I convinced them: I was like "look how many times you say 'if the Widget is type 2, do this stuff, but if type 1, do that stuff' in that paragraph - and the difference in 'this' and 'that' is exactly just the difference in the number of attached Thingums" elegant generalization = no separate cases = fewer ifs i don't know if this makes my argument sound more or less compelling but i think if we'd had our current "if statements are bad / minimize parallel worlds" principle it would've kept us from introducing premium plans, which we now in fact regret introducing. https://blog.beeminder.com/focus/

dreeves commented 1 year ago

Another anti-magic case study, about failing fast: https://github.com/beeminder/blog/issues/376#issuecomment-1736259293

dreeves commented 5 months ago

New potential case study in zzq's BeeBrowse browser extension for collapsing graphs on one's Beeminder dashboard: https://forum.beeminder.com/t/my-beeminder-browser-extension/6648/58?u=dreev