{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/python/tame-conditionals-with-bitmasks/",
"description": "Replace complex nested conditionals with Python's enum.Flag and bitmasks for cleaner, more maintainable logic using bitwise operations.",
"path": "/python/tame-conditionals-with-bitmasks/",
"publishedAt": "2023-07-29T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Python",
"TIL"
],
"textContent": "The 100k context window of [Claude 2] has been a huge boon for me since now I can paste a\nmoderately complex problem to the chat window and ask questions about it. In that spirit, it\nrecently refactored some pretty gnarly conditional logic for me in such an elegant manner\nthat it absolutely blew me away. Now, I know how [bitmasks] work and am aware of the\nexistence of [enum.Flag] in Python. However, it never crossed my mind that flags can be\nleveraged to trim conditional branches in such a clever manner that Claude illustrated. But\nonce I looked at the proposed solution, the whole thing immediately clicked for me.\n\nThe conundrum\n\nHere's a problem that's similar to what I was trying to solve. Let's say we have instances\nof a Client entity that need to be notified when some special event occurs in our system.\nThe notification can happen in three ways: email, webhook, and postal mail. These are the\nthree attributes on the Client class that determine which notification method will be\nused:\n\nThe business logic requires that the system must abide by the following rules while sending\nnotifications:\n\n- If only email is populated, send an email.\n- If only url is populated, send a webhook.\n- If only address is populated, send a postal mail.\n- If email and url are populated, send an email and a webhook.\n- If email and address are populated, only send an email.\n- If url and address are populated, only send a webhook.\n- If all three are populated, send both an email and a webhook.\n- At least one attribute must be populated, or it's an error.\n\nNotice how the business logic wants to minimize sending notifications via postal mail.\nPostal mails are expensive and will only be sent if address is the only attribute on the\nClient instance. In any other cases, emails and webhooks are preferred.\n\nFirst shot\n\nThe notify function takes in a Client object and sprouts a few conditional branches to\nsend notifications while maintaining the business constraints.\n\nWhoa! Lots of if-else branches for such a simple scenario. Since there are 3 attributes in\nthe complete _set_, we have to make sure we're writing 2^3=8 branches to cover all the\npossible _subsets_. For 4, 5, 6 ... attributes, the number of branches will increase as\npowers of 2: 2^4=16, 2^5=32, 2^6=64 ... and so on. Then our tests will need to be able\nto verify each of these branches. We can try to apply De Morgan's law to simplify some of\nthe negation logic.\n\n> De Morgan's laws allow us to take the negation of a conditional statement and distribute\n> it across the operators, changing ANDs to ORs and vice versa, and flipping the negation of\n> each component. This can help simplify complex boolean logic statements.\n\nSo this:\n\nCan become:\n\nHowever, that still doesn't reduce the number of branches. Bitmasks can help us to get out\nof this pothole.\n\nA quick primer on bitwise operations & bitmasking\n\nBitwise operations allow manipulating numbers at the individual bit level. This is useful\nfor compactly storing and accessing data, performing fast calculations, and implementing\nlow-level algorithms. Here's a list of bitwise operations:\n\n- Bitwise AND (&): Takes two numbers and performs the logical AND operation on each pair\n of corresponding bits. Returns a number where a bit is 1 only if that bit is 1 in both\n input numbers.\n\n- Bitwise OR (|): Takes two numbers and performs the logical OR operation on each pair\n of corresponding bits. Returns a number where a bit is 1 if that bit is 1 in either or\n both input numbers.\n\n- Bitwise XOR (^): Takes two numbers and performs the logical XOR (exclusive OR)\n operation on each pair of corresponding bits. Returns a number where a bit is 1 if that\n bit is 1 in exactly one of the input numbers (but not both).\n\n- Bitwise NOT (~): Takes a single number and flips all its bits.\n\n- Left shift (<<): Shifts the bits of a number to the left by a specified number of\n positions. Zeros are shifted in on the right. Equivalent to multiplying by 2^n where n\n is the number of positions shifted.\n\n- Right shift (>>): Shifts the bits of a number to the right by a specified number of\n positions. Zeros are shifted in on the left. Equivalent to integer division by 2^n.\n\nHere's an example displaying these operators:\n\nBitmasks are integers that represent a set of flags using bits as boolean values. Bitmasking\nuses bitwise operators to manipulate and access these flags. A common use of bitmasks is to\ncompactly store multiple boolean values or options in a single integer, where each bit\nposition has a specific meaning if it is 1. In the next section, we'll use this capability\nto clip the conditional statements in the notify function.\n\nFor example, here's a bitmask representing text style options:\n\nWe use powers of 2 (1, 2, 4, 8, etc.) for the flag values so that each bit position\ncorresponds to a single flag, and the flags can be combined using bitwise OR without\noverlapping. This allows testing and accessing each flag independently:\n\nAnd toggle an option on or off using XOR:\n\nYou can do a ton of other cool stuff with bitwise operations and bitmasks. However, this is\npretty much all we need to know to curtail the twisted conditional branching necessitated by\nthe business logic. Check out this incredibly in-depth [Python bitwise operators article]\nfrom Real Python if you want to dig deeper.\n\nPruning conditional branches with flags\n\nWith all the intros and primers out of the way, we can now start working towards making the\nnotify function more tractable and testable. We'll do that in 3 phases:\n\n- First, we're gonna define a flag-type enum called NotifyStatus which will house all the\n valid states our notification system can be in. Any state that's not explicitly defined as\n an enum variant is invalid.\n\n- Second, we'll write a function named get_notify_status that'll take in a Client object\n as input, apply the business logic and return the appropriate NotifyStatus enum variant.\n This function won't be responsible for dispatching the actual notification handlers;\n rather, it'll just map the attribute values of the Client instance to a fitting enum\n variant. We do this to keep the core business logic devoid of any external dependencies -\n following Gary Bernhardt's [functional core, imperative shell] ethos.\n\n- Finally, we'll define the notify function that'll just accept the enum variant returned\n by the previous function and invoke the desired notification handlers.\n\nThe NotifyStatus enum is defined as follows:\n\nHere, the EMAIL, URL, and ADDRESS variants correspond to the eponymous attributes on\nthe Client instance. Then we define the composite variants (bitmasks) to compactly\nrepresent the valid states the system can be in. For example, EMAIL_URL = EMAIL | URL\nmeans that on the Client instance, email and url attributes are populated but\naddress isn't. Likewise, EMAIL_URL_ADDRESS denotes that all the attributes are\npopulated. The biggest benefit we get from this is that we don't need to write the negation\nlogic explicitly; the bitmasks encode that information inherently. This representation will\ngrossly simplify the implementation of the business logic.\n\nNow, let's write the get_notify_status function that'll take in an instance of Client\nand return the appropriate NotifyStatus variant based on our business logic:\n\nThis is the full implementation of our business logic in its entirety. It checks which of\nthe notification attributes among email, url, and address are populated on the\nClient object. For each one that is populated, it picks the corresponding variant from the\nNotifyStatus enum and sets the variant bit in the status integer using bitwise OR. If all\nthree attributes are empty, it raises a ValueError. The final value of status is then used\nto return the correct NotifyStatus enum variant.\n\nOn the last step, the notify function can take the NotifyStatus variant returned by the\nget_notify_status function and dispatch the correct notification handlers like this:\n\nObserve how we've totally eliminated conditional statements from the notify function. The\nkey takeaway here is that the program flow is now flatter and easier to follow. The core\nbusiness logic is neatly tucked inside the get_notify_status routine, and the\nNotifyStatus enum explicitly defines all the valid states that the system can be in. This\nalso means that if a new notification channel pops up, all we'll need to do is update three\nflat constructs and write the corresponding tests instead of battling with the twisted\nconditional statements that we started with. Not too shabby, eh?\n\n\n\n\n[claude 2]:\n https://www.anthropic.com/index/claude-2\n\n[bitmasks]:\n https://stackoverflow.com/questions/10493411/what-is-bit-masking\n\n[enum.Flag]:\n https://docs.python.org/3/library/enum.html#enum.Flag\n\n[python bitwise operators article]:\n https://realpython.com/python-bitwise-operators/\n\n[functional core, imperative shell]:\n https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell",
"title": "Taming conditionals with bitmasks"
}