Edge Case Poisoning (2020)
buttondown.email
buttondown.email
Some might say that for processes this means everything must be extremely abstract in order to avoid edge cases like those encountered by the author. However, I would argue that from the perspective of someone executing a recipe they do not care at all about whether something is edible or food or not, they only care how much they need of something (count is a unit(less) of measure). Thus, the ontology proposed by the author is not matched to the domain.
The first mistake was trying to make a distinction between food and non-food. What if I used paper cupcake cups? They may not technically be food, but I have certainly eaten parts of them before by accident. Other parts of the system might care about food/non-food, but these parts are constrained by a separate and likely orthogonal set of use cases.
I don't usually need to know the chemical formula for sodium bicarbonate to order it from a vendor, but if I need to automatically calculate stoichiometry for reactions so that I can automatically order the correct amount then I might. Those two parts of the system can and should be completely orthogonal to each other and thus fully decoupled.
Therefore, I would suggest that encountering something that looks like "edge case poisoning" is a sign that you have not properly factored the system.
Take betting as an example I work on. The basic idea is that if a bet wins you get paid your stake multiplied by the odds of the bet. If I open the codebase it should be trivial to find where that multiplication happens right? It's such a fundamental part of the code. But actually the edge cases (starting price bets, each-way betting, handicap betting with split line handicaps, multiple bets, dividend bets) mean it's very difficult to point to exactly where that happens. If I had to guess 90% of the code isn't needed at all in the majority of bets which are singles or straight accumulators.
Most in-game items and resources were very simple - largely expressible by a simple <item_type>:<quantity> dictionary. Others however, could support a never-ending variety of custom attributes and logic.
After much thought, I wound up pursuing a solution that turned out to be quite powerful and extendible:
Items would be represented via a <item_type>:<<attr_key>:<attr_value>> datastructure. Any system that needed to interact with an item would call an 'item handler' assigned to that item type, which exposed a standard interface like 'getQuantity', 'useItem', etc.
Most basic items shared a common handler that stored quantity as an attribute field. However, more complex items could implement custom logic. I guess this is somewhat similar to Mixins or Component Based Architecture.
I think this is partially covered under the 'more abstraction' option in the blog, but I've personally found this to be an interesting and valuable tradeoff that can be deployed in a lot of situations.
But I'm also a bit skeptical about how far you can push the idea beyond the kinds of design tradeoffs people already make. It's often hard to be certain about which edge cases will prove to be important or valuable in the future, and when you cut this sort of corner, changing your mind later can sometimes be very, very expensive (e.g.: I've experienced the pure agony that comes with migrating away from ancient mainframes, which handled every single edge case in plaintext). Not to mention that in large systems, every tiny edge case ends up being useful to a huge number of people anyway.
Perhaps this term is helpful in the same way that "technical debt" is: it encodes a framing that the desire for hygiene or completeness should be balanced thoughtfully in terms of user benefit and added complexity.
One of the root causes is that when you're designing something, you really suck at understanding costs of your solution.
To you, the cost either seems close to zero (because you are familiar with your own solution), or cost doesn't matter because you think you're solving problems that are essential to domain.
Somewhat counterintuitive, the more senior folks are, the more likely they are to be affected by it.
If the goal is to publish the recipe on a website for humans to see, you just need the raw blob of text.
If the goal is to calculate calories, you can drop anything that isn't edible, focus on ingredients with a standard unit of measure, and notify the user if the recipe contains ingredients for which the calorie contribution couldn't be determined.
If the goal is automated production, all of this is likely irrelevant.
Etc...
If your goal is to cram types into things to make them cool, unsurprisingly this goal does not give you a basis to make design decisions.
- George Berkley (apocryphal)
It's fine to handle an explicit subset of a problem. What's not fine is not making this clear. Real software can generally have a well-defined domain and range, which this article muddies with its example.
> This works but is quitter talk.
It's a valid solution. Expressing weird recipes as lists of normal recipes is perfectly fine.
> If we want to include recipe expansion as a feature of our model, we need to make the algorithm more complicated to handle the single edge case of fondant.
Yes, it turns out writing correct software sometimes requires this, even for a small amount of overall cases. That's no argument against not handling all cases.
To the parent poster:
I expect someone incapable of writing perfect software to claim it to be impossible, yes. I'd upload an article I've written proving my point, but it's about correct software, so it would just get ignored here.
Software is mathematics, and mathematics can be perfect.
> This is why I laugh at talk of "bug free" software. The best you can do is zero reported bugs. Temporarily.
No, this is the best the incompetent can do, but I won't let them drag me down to their level.
Eg: Sat imagery is scan lines full of instrument return values.
The first normalisation is to use 99.9% of the returned value range to setup a colour lookup table.
.1% of the return values could be :
* lens flare
* instrument error
* actual valid but extreme data values.
Depending on the problem domain, after removing (actual) error and bogus values (where possible) you might actually be using the 99.9% of the data to "train" for normal expected background stuff ... and you're really looking for the edge case that is Gold | Uranium | a Hidden tank, etc.
Does this means that e.g. you just ignore the recipes with optional ingredients, or "normalize" them by making them mandatory (or deleting them), or you create two recipes (one with and one without the optional part)?
My point is, consider the problem space, domain, users when thinking about this stuff. Type systems don’t exist in isolation.
Lots of recipes have optional ingredients. I don't think this person is very familiar with cooking. All cooking apps handle optional ingredients fine. Nobody would use the one you finna make that doesn't have that.
This is the worst I've been insulted in my life and I demand you duel me in a cookoff. Skillets at dawn! ;)
I actually just bake stuff from recipes once in a while. Sounds like you're actually a lot more experienced than me.
As several people pointed out, I never said what my context for encoding recipes was, so here's a bit more about what inspired this example. I throw a lot of dinner parties, and a lot of my friends have dietary restrictions. I always try to pick a menu where everybody can eat at least one entree and at least one side.¹ I want to be able to query my recipes for "vegan-friendly" or "peanut-free". But also a lot of recipes have substitutions: if a pork ingredient can be replaced by tofu, does that dish count as "vegetarian"? Maybe, maybe not, but it'd be nice to have the option to choose.
(The other menu constraint is the cooking process: I can't make two dishes that both take the slow cooker, but I can do two that both take skillets.)
I occasionally look for recipe apps but I never find any that are good for this use case. For now I still print out recipes and put them in a binder and figure out the menus on a whiteboard. So ultimately it's more an interesting example than a problem I'm trying to solve.
¹ I love them to death but I also a breath a sigh of relief when none of my vegan friends can make it
Weaken the type system, reduce the automated test coverage, and you are likely to find that edge cases are addressed on more of a "who's currently yelling at us about what feature isn't working" basis, which will ultimately mean that fewer edge cases get addressed or are more likely to persist as undetected bugs, which could be either desirable or undesirable, depending on your domain.
Yes, the original product may be bloated / not pretty, but once paying users discover you don't support an obscure edge case they depend on, they'll stop showing up.
It turns out Larry started by selling to people doing what would now be called "Business Intelligence", and for them Durable wasn't a necessity, it was just an edge case (they were always side-loading from production anyway).
Oracle then used the profits from this beachhead to fix their durability issues before they started selling into segments that expected their databases to, you know, keep data.
I didn't know that Oracle itself did exactly that, thanks. But I'm talking about the days of mature Oracle.
The downside of this approach is that for subtrees of shared behavior you can go the multi-level inheritance route (risky if you're not sure the leaves will hold their parent's contract) accept the extra boilerplate for similar behavior.
It's interesting to me how this happens quite often and polymorphism is still our go-to solution.
I think one pitfall that a lot of software designers fall into is assuming they can know the entire problem domain up front. Maybe that works for a super-mature industry like airline reservations or something. But I still tend to doubt it.
In my experience you constantly get stuff that borks your data model after going live. I always assume this will happen continuously throughout the lifecycle of the product, and try design accordingly.
So here's a concrete example: the config file / structure for a virtual machine. Your basic VM has a # of cpus, an amount of memory, a virtual disk, and a virtual network card. Oh, but this VM is actually a "service VM" that is providing an emulated device for another VM. And this VM is actually a fast, ephemeral clone of another VM: it has copy-on-write memory and isn't allowed to write to the disk. And this VM is a live-snapshoting clone of a remote VM: it doesn't execute, but just receives memory and disk updates from the remote VM, until the heartbeat is lost, and then continues. Oh, and this VM's disk is actually provided over the network by a SAN. Oh, and...
The result being that if, like 95% of people, you just want to make a plain VM, you have to wade through a massive list of who-knows-what options to make it work. Balancing making it simple for those 95%, while functional for the other 5%, is a challenge.
At least, not without restricting yourself to a tractable subset like dependabot does.