DEV Community

Cover image for Bitmasks: A very esoteric (and impractical) way of managing booleans
Basti Ortiz
Basti Ortiz

Posted on • Edited on

Bitmasks: A very esoteric (and impractical) way of managing booleans

2024 July 20: Six years and one computer science degree later, I have since changed my mind on most of the negative sentiments in this article. See the embedded article below for my updated thoughts in the form of an appreciation post for bitmasks.


Have you ever asked yourself what bitwise operators are for? Why would such a high-level language such as JavaScript ever need such a low-level operator? For one, it actually has its use cases in JavaScript. Most are just not as obvious as others. Actually, most are not even obvious at all unless you really try and squint your eyes at the computer screen. Trust me, I have tried. I'm not even joking. Throughout my relatively short experience with JavaScript (3 years as of writing this article), it has been so rare to find instances of bitwise operators appear in average situations. I might not be looking deep enough, but it does seem pretty clear to me why this is so. By the end of this article, you will see why this is the case.

Bitwise Operators

NOTE: I am not requiring an extensive knowledge on the subject, but I will assume that you are already at least somehow familiar with binary number systems and bitwise operators. If not, I highly recommend that you read up a bit (See what I did there?) before continuing with the rest of this article.

Bitwise operators allow us to manipulate the individual bits that make up a number in binary. For a quick review, here is a "table" of what the common bitwise operators do.

// I will use the binary notation prefix ("0b") a lot in this article.
const num1 = 0b1010; // 10
const num2 = 0b1111; // 15

// NOT num1
~num1; // 0b0101 (complement) === -11

// num1 AND num2
num1 & num2; // 0b1010 === 10

// num1 OR num2
num1 | num2; // 0b1111 === 15

// num1 XOR num2
num1 ^ num2; // 0b0101 === 5

// Bit-shift to the left by 1
num1 << 1; // 0b10100 === 20

// Bit-shift to the right by 1
num >> 1; // 0b0101 === 5
Enter fullscreen mode Exit fullscreen mode

I mean this is great and all for the sake of learning something new everyday, but when would you ever use this knowledge? Is there a practical application for bitwise operators? Short answer, no. Although it can be useful in code minification, memory optimization, and some other use cases, by using bitwise operators, you are opting into less readable code. It is just more cryptic to read because you have to set your "Decimal Mode" brain into "Binary Mode". Nonetheless, that doesn't stop us, right? We're all here to learn. So without further ado, I present bitmasks.

Over-engineering a simple problem

Honestly, I don't have a simple definition for what a "bitmask" is. It's quite a strange monster if you'd ask me. To me, a bitmask can be thought of as a query. Using a bitmask means to query the bits found in some binary number. If you're confused by that definition, I don't blame you. I have to admit that it isn't the best definition. If you can think of a better one, please do leave a comment below. I'd gladly update this article to specifically include your definition.

Anyway, a definition is worthless without its complementary example. Let's say we have an object that stores booleans corresponding to the configurations found in an app.

// Mock app settings
const config = {
  isOnline: true,
  isFullscreen: false,
  hasAudio: true,
  hasPremiumAccount: false,
  canSendTelemetry: true
};
Enter fullscreen mode Exit fullscreen mode

Our job is done at this point. We can store that as is in a JSON file. That is the straightforward implementation. However, we can use bitmasks to "over-engineer" this problem. In JavaScript, number types can be explicitly converted (or coerced) into booleans by passing it into the Boolean function. Take note that in this case, Boolean is not used as a constructor. It is simply a means to convert the number type (or any type actually) into its equivalent boolean "truthiness". For example:

Boolean(-2); // true
Boolean(-1); // true
Boolean(0); // false
Boolean(1); // true
Boolean(2); // true
Boolean(Math.PI); // true
Boolean(Number.MAX_SAFE_INTEGER); // true
Enter fullscreen mode Exit fullscreen mode

Since 0 is not exactly a "truthy" value per se, it evaluates to false. That relationship gives us an idea on how to convert a bunch of booleans into a single number. Instead of storing the app settings as an object, we can store it as a single number. Yup, you heard, or rather read, that right. First, we think of the booleans as 1s and 0s, where 1 is true and 0 is false. These 1s and 0s correspond to each property in the config object from left to right.

// For reference only
const config = {
  isOnline:          true,
  isFullscreen:      false,
  hasAudio:          true,
  hasPremiumAccount: false,
  canSendTelemetry:  true
};

// isOnline:          1
// isFullScreen:      0
// hasAudio:          1
// hasPremiumAccount: 0
// canSendTelemetry:  1
// Thus, we have the binary number 0b10101.
let configNumber = 0b10101; // 21
Enter fullscreen mode Exit fullscreen mode

Bitmasks

NOTE: Here comes the weird part of the article. This is where I pull out the black magic. I hope you have stretched those brain muscles enough because you'd be doing a strenuous workout with it from this point on. Feel free to read some parts over and over again. This is a pretty difficult topic to say the least.

Now that we have reduced an entire object into a single number, we can use bitwise operators on it. But why, do you ask? Well, this is the essence of bitmasking.

A bitmask is a way to "select" the bits you're interested in. When selecting a single particular bit, it is always a power of 2 because any power of 2 corresponds to that particular bit that is "turned on". Since bit-shifting to the left is essentially multiplying by 2 (analogous to raising 2 by a power), you can think of bit-shifting to the left as a way to "select" the bit you are interested in.

// Selecting the 1st bit from the right
// 2 ** 0
// 1 << 0
0b00001 === 1;

// Selecting the 2nd bit from the right
// 2 ** 1
// 1 << 1
0b00010 === 2;

// Selecting the 3rd bit from the right
// 2 ** 2
// 1 << 2
0b00100 === 4;

// Selecting the 4th bit from the right
// 2 ** 3
// 1 << 3
0b01000 === 8;

// Selecting the 5th bit from the right
// 2 ** 4
// 1 << 4
0b10000 === 16;
Enter fullscreen mode Exit fullscreen mode

If we want to select more than one bit, we can do that, too.

// Selecting the 1st and 5th bit from the right
0b10001 === 17;

// Selecting the 3rd and 4th bit from the right
0b01100 === 12;

// Selecting the 2nd, 4th, and 5th bit from the right
0b11010 === 26;

// Selecting the 1st, 2nd, and 4th bit from the right
0b01011 === 11;

// Selecting ALL the bits
0b11111 === 31;
Enter fullscreen mode Exit fullscreen mode

Getting Values

Bitmasking allows us to extract the value of a single bit in the configNumber number. How do we do this? Let's say we wanted to get the value of hasAudio. We know that the hasAudio property is located at the third bit from the right of the configNumber.

let configNumber = 0b10101; // 21

// Shifting 0b1 to the left 2 times gives the 3rd bit from the right
const bitMask = 0b1 << 2; // 4

// Since we know that the 3rd bit from the right corresponds to the hasAudio property...
const query = configNumber & bitMask; // 4

// ...we can test its "truthiness" by using the AND operator.
const truthiness = Boolean(query); // true

// The truthiness IS the value we want to extract.
truthiness === config.hasAudio; // true
Enter fullscreen mode Exit fullscreen mode

At this point, you may be asking...

"So what if it returns 4?
"So what if it's coerced to true?"
"So what if it's 'truthy'?"

If you are asking that, then you just answered your own question. 4 has been coerced to true. That is the exact value of the hasAudio property in the original config object. We have successfully extracted the value of the hasAudio property through bitmasking.

Well, what happens if we try to query a "falsy" property such as isFullscreen? Would bitmasking reflect the same value in the original config object? As a matter of fact, it does. We know that the isFullScreen property is located at the fourth bit from the right in the configNumber.

let configNumber = 0b10101; // 21

// Shifting 0b1 to the left 3 times gives the 4th bit from the right
const bitMask = 0b1 << 3; // 8

// Since we know that the 4th bit from the right corresponds to the isFullscreen property...
const query = configNumber & bitMask; // 0

// ...we can test its "truthiness" by using the AND operator.
const truthiness = Boolean(query); // false

// The truthiness IS the value we want to extract.
truthiness === config.isFullscreen; // true
Enter fullscreen mode Exit fullscreen mode

We can go even crazier by selecting multiple bits in our bitMask, but I'll leave that as an exercise for you to ponder on.

You may be noticing a pattern here. The result of the AND bitwise operator determines the truthiness of a query. The truthiness is essentially the actual value of the property we are trying to get in the first place. Yes, I know; it's black magic. I had the same reaction. It was too clever for me to fully comprehend at the time.

So now that we know how to extract a boolean out of a specific bit, how do we manipulate a bit?

Toggling Values

The same logic follows when we want to toggle bits. We still use bitmasks to select the bits we're interested in, but we use the XOR bitwise operator (^) instead of the AND bitwise operator (&) for our query.

Let's say we wanted to toggle the canSendTelemetry property. We know that it is located in the first bit from the right.

let configNumber = 0b10101; // 21

// Shifting 0b1 to the left 0 times gives the 1st bit from the right,
// which corresponds to the canSendTelemetry property
const bitMask = 0b1 << 0; // 1

// Toggling the 1st bit from the right
const query = configNumber ^ bitMask; // 20

// Setting the query as the new configNumber
configNumber = query;
Enter fullscreen mode Exit fullscreen mode

Now if we tried to extract the canSendTelemetry property from the new configNumber, we will find that it is no longer set to true. We have successfully toggled the bit from true to false (or rather from 1 to 0).

All Together Now

This is definitely tedious to do over and over again. Since we all want to save a few keystrokes, let's create some utility functions that does all this for us. First, we will write two utility functions that extracts the "truthiness" of a bit: one extracts the "truthiness" if it's given a bitmask, while the other extracts the "truthiness" if it's given the zero-indexed position (from the right) of the bit being extracted.

/**
 * Extracts the "truthiness" of a bit given a mask
 * @param {number} binaryNum - The number to query from
 * @param {number} mask - This is the bitmask that selects the bit
 * @returns {boolean} - "Truthiness" of the bit we're interested in
 */
function getBits(binaryNum, mask) {
  const query = binaryNum & mask;
  return Boolean(query);
}

/**
 * Extracts the "truthiness" of a bit given a position
 * @param {number} binaryNum - The number to query from
 * @param {number} position - This is the zero-indexed position of the bit from the right
 * @returns {boolean} - "Truthiness" of the bit we're interested in
 */
function getBitsFrom(binaryNum, position) {
  // Bit-shifts according to zero-indexed position
  const mask = 1 << position;
  const query = binaryNum & mask;
  return Boolean(query);
}
Enter fullscreen mode Exit fullscreen mode

Finally, let's write a utility function for toggling one or multiple bits. The function returns the new binaryNum that comes as a result of toggling the selected bits.

/**
 * Returns the new number as a result of toggling the selected bits
 * @param {number} binaryNum - The number to query from
 * @param {number} mask - This is the bitmask that selects the bits to be toggled
 * @returns {number} - New number as a result of toggling the selected bits
 */
function toggleBits(binaryNum, mask) {
  return binaryNum ^ mask;
}
Enter fullscreen mode Exit fullscreen mode

We can now use these utility functions with the previous examples.

const config = {
  isOnline:          true,
  isFullscreen:      false,
  hasAudio:          true,
  hasPremiumAccount: false,
  canSendTelemetry:  true
};
let configNumber = 0b10101;

// Extracts hasPremiumAccount
getBits(configNumber, 1 << 1); // false
getBitsFrom(configNumber, 1); // false

// Toggles isOnline and isFullscreen
toggleBits(configNumber, (1 << 4) + (1 << 3)); // 0b01101 === 13
Enter fullscreen mode Exit fullscreen mode

Conclusion: Why should I even bother with bitmasking?

That's a very good question. Frankly, I wouldn't recommend using this regularly, if at all. As clever as it is, it is just too esoteric for common use. It is impractical and unreadable most of the time. Constant documentation and awareness are required to make sure that the correct bits are being selected and manipulated. Overall, there aren't many applications for this, especially in a high-level language like JavaScript. However, that shouldn't discourage you from using it if the need arises. It is our job as programmers to determine which algorithms are the best for both the user (for usability) and the developer (for maintainability).

If that is so, then what is the point of me writing an entire article about this?

  • This is for the hardcore computer scientists out there. They are the ones who will benefit the most from this article, especially those who are just beginning to dive deeper into the weird world of computer science. To put it more generally, one does not need to be a computer scientist to benefit from this article. Whoever is interested in such topics will see the value in all of this bitmasking chaos.
  • For those who are not into computer science, you now have more tools under your belt. You can use bitmasks in the future if the time calls for it. I hope this article encourages you to think creatively. Over-engineering is a curse we all suffer eventually. It isn't entirely a bad thing, though. Over-engineering is just a negative connotation for thinking (too) creatively. Our brains tend to explore ideas even if it's impractical. Of course, we have to avoid it for productivity, but a little exploration now and then is always healthy. Get that brain working, and that brain would work for you.
  • As for me, I wrote this article to test myself. I wanted to know how much I've learned so far. Besides that, I find pleasure in teaching others. One can learn so much by teaching others. This is the primary reason why I write articles for this site. It just has its rewards, you know? If you aren't already, go ahead and try to teach someone something new. It might surprise you to see how much it can help you as well.

Bitmask responsibly.

Top comments (30)

Collapse
 
zenmumbler profile image
zenmumbler • Edited

While JS is a high-level language and it initially certainly wasn't meant to be used for much more than some simple script blocks, the engineering and runtimes that we use this language in have lowered things considerably.

Consider that JS runtimes have multiple tiers of JS code processing, the latter ones compiling it into direct assembly and throwing optimiser passes over it. JS engines detect and optimise shapes of objects to eliminate all the handling around string lookups for the keys of your objects.

So, while you can still use JS for everyday high-level tasks, you can now successfully use it for very high data throughput operations as well, both in the browser and in server contexts. And memory usage and data layout impacts throughput significantly. Consider this made up but actually kind of realistic metadata object:

const stuff = { first: true, group: 20, sortKey: 347843 };
Enter fullscreen mode Exit fullscreen mode

To create this object, a JS engine has to allocate a standard JS object, allocate 3 strings for the keys and store the 3 values. Now if there are many objects being handled like this then JS engines will get smarter about them, but they still have to create new dynamically allocated objects for each one.

BTW, with "many" I'm thinking of the tens or hundreds of thousands or larger magnitudes.

Given that I know as the developer of this app that the sortKey field is never larger than, say, 1 million and the group is at most 50 and the first is a boolean, which is 1 or 0.

I can use this info to put all this info into a single 32-bit integer. sortKey can fit inside 20 bits (2^20 > 1M), group can fit into 6 bits (2^6 = 64) and first can fit into a single bit, total 27 bits. I can store these as such:

bit number (hi to lo)
 3         2         1        
10987654321098765432109876543210
--------------------------------
00000FGGGGGGSSSSSSSSSSSSSSSSSSSS
Enter fullscreen mode Exit fullscreen mode

Where F = first, G = group and S = sortKey

The above object can then be represented as:

const stuffAsInt = 0x05454ec3; // hexadecimal representation
Enter fullscreen mode Exit fullscreen mode

To create this value, the JS engine has to … allocate nothing (in optimised passes), it's just an integer that fits comfortably in a register inside the CPU, we even have 5 bits to spare! ;) I can now use the bit masking and insertion techniques you mentioned to read/write data in these values (did I mention that bit ops are single instructions on the CPU?)

Now, if this was indeed only for a single variable, we wouldn't have gained much, but we're talking about large amounts of data here. Say, I will process 1 million of these blocks at a time. To do that I can:

const block = new Uint32Array(1 * 1000 * 1000);
Enter fullscreen mode Exit fullscreen mode

And done, all memory accounted for, and with simple indexing I can access each element.

Doing the same for the JS object variant would mean:

  • millions of allocations of keys, objects and values
  • a ton of GC (garbage collection) cleanup after each iteration
  • much larger and potentially fragmented memory usage that modern CPUs really don't like.

The first 2 can be moved to init time with object reuse, but the 3rd one is maybe even more important. Modern CPUs really don't like cache misses and fragmented memory reads, it can have a significant impact on the runtime of your code.

Either way, the end result is that by representing the data more efficiently the code will run (much) faster and take less memory, which means being potentially better than competition, having lower server costs (fewer servers needed with less memory in each), not having to wait for results, being able to handle bigger workloads, etc.

So again, for "normal" usage indeed, don't bother, but for high-volume data processing it really pays to look at data layout optimisation, and bit fields are one of the tools you can use for that!

Collapse
 
yuripredborskiy profile image
Yuri Predborskiy

This optimization is fantastic. And horrific at the same time. It can provide performance improvement seemingly out of thin air. And it can be a nightmare to support for anyone other than the author. What if the number of groups changes dramatically? Like, grows to 100, or 200? Or is removed entirely? What if first is replaced by 6 booleans? What if... the metadata changes completely?

My point: bitwise operations are a great tool, but it has extremely specific usage cases. I'd advice average software developers to be extremely skeptical about it (don't use it unless you know what you want to achieve and there's no better way, or it requires significant sacrifices elsewhere).

Collapse
 
leoweyles profile image
LeoWeyles • Edited

And it can be a nightmare to support for anyone other than the author.

Not really, especially if you're used to it. It is common practice to assign each flag to a constant in order to improve readability, rather than using the numbers directly.

it has extremely specific usage cases

Bitmasks are quite common in systems programming and binary file formats. Several UI toolkits (Qt, Gtk...) also use them. They can also be used to implement certain mathematical functions on machines that don't have a FPU.

There's life outside web development.

Collapse
 
somedood profile image
Basti Ortiz

I really love the example you gave. It's very interesting. I applaud the creativity and cleverness displayed in those 27 bits. I just learned something new and cool today. Thanks!

Collapse
 
kpentaris profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
kpentaris

Stop confusing people with weird words like "CPU" and "register", what's wrong with you? Did you even read the article and especially the context in which it was written?

Collapse
 
kl13nt profile image
Nabil Tharwat

I really hope you're being satire.

Collapse
 
admtomas profile image
Tomasz Amanowicz

I am new to the software development and we use bitwise masking. It's a real nightmare for newbies as code is totally unreadable. At least I might understand some basics now.

Collapse
 
moopet profile image
Ben Sinclair

Permissions are the only real way most people will see bitmasks these days unless you're doing something particularly low-level.
Like file permissions:

rwxr-xr-x

reads literally as

1111101101 (i.e. 755)

Not realising this is exactly why everyone abuses chmod -R 777 so wildly.

It's also good1 for wrapping up complex permissions in a program:

function isLivingRelative(person) {
  return person.isAlive && person.isRelative;
}

function canBorrowMyCar(person) {
  return isLivingRelative(person) || person.wasQuickWithTheLawmowerThatOneTime;
}
Enter fullscreen mode Exit fullscreen mode

vs.

const isAlive = 1;
const isRelative = 2;
const wasQuickWithTheLawmowerThatOneTime = 4;
const ISLIVINGRELATIVE = isAlive | isRelative;
const CANBORROWMYCAR = ISLIVINGRELATIVE | wasQuickWithTheLawmowerThatOneTime;
Enter fullscreen mode Exit fullscreen mode

  1. Meh, "good". 

Collapse
 
somedood profile image
Basti Ortiz

I was supposed to include an example on complex conditions in the article, but then I thought that it was probably getting too long already. Thanks for bringing this up!

Also, may I ask what the xs stand for in rwxr-xr-x?

Collapse
 
moopet profile image
Ben Sinclair

They're the execute bits - if it's a file, you can run it as a command, if it's a directory, you can cd into it.

Collapse
 
renoirb profile image
Renoir • Edited

Hey, your article was very useful. Thank you for that. I think it's useful for use-cases when you have lists of booleans and you want to compare against. Instead of diffing in loops "foo === arr[i] && true === arr[i]" having into numbers is simpler.

I've expanded and made a class for making it simpler to use. I've called it tuples-boolean-bitmasker, the source is in a project on a GitLab monorepo I own, here are the unit tests I've written while building BitMasker

Collapse
 
bosley profile image
Bosley • Edited

I feel like this article and others here on DEV regarding bitwise operations are spreading fear about something quite simple. I think that the undertones of the article may make readers more likely to panic if they run into bitwise operations in the wild. I use them quite often for work, and its really not bad. Its just a thing. I do appreciate the depth of the article though, good post!

Collapse
 
ludeed profile image
Luís Silva

Great article! If you allow, I will add my example of bitmasking and a very cool one imho

When it comes to data compression algorithms many times you need to write or read just one bit. In nowadays computer systems that kind of granularity is impossible because the minimum amount of data is a byte. So if it weren't for bitmasking and binary operators in language compression algorithms would be very useless.

Collapse
 
somedood profile image
Basti Ortiz

Sometimes, I feel like bit manipulation is an underappreciated art. Your example proves it. We use compression algorithms everyday, especially in the Web where all network requests must be minimized as much as possible, yet there is little attention and appreciation given to it.

Collapse
 
antoneb profile image
AntoneB

I don't do much programming in Java script and I don't know why one would use bit masks in a language that is mostly for browser side high level coding.

But calling them esoteric and impractical would lead me to believe you don't deal with embedded coding and good old standard c. Often in embedded systems we can't afford to be wasteful with resources, since c has a much smaller compiled foot print than C++ and higher languages it is used.

Also when working with creating a client server protocol that sends and receives data packets (like a multi player game), you often need to send flags that say things like jump, attack, crouch, move, etc..

You could create a struct to send as a packet using a bunch of C++ BOOL's. But here's the rub, if you're using a bool per flag, a bool requires minimum 1 Byte or 8 bits; however, if you use an unsigned int as a bitfield, and bit masks, you can effectively have 8 bools per byte for a char, or uint8_t.

You get 8 times the bool flags, with 1/8th the data packet size. Reducing the data packet size reduces the latency. Also bitwise operators are very fast for the cpu to process.

For Java Script, I'd probably not bother with bitmasks and bitwise operators, but if you are using c or c++ and trying to minimize the amount of memory you're using, bit masks will always beat bool.

Collapse
 
somedood profile image
Basti Ortiz

Yup, you are definitely correct there. Perhaps I have been too harsh on bitmasks. I never really considered embedded systems when I wrote this article. I simply had a "JavaScript mindset", so to speak.

Collapse
 
guneyozsan profile image
Guney Ozsan • Edited

Definitely not an expert but my take away from my limited experience with bit masks are like this:

  1. Bitmasks can help abstraction by not exposing the object(s) being set.
  2. They are helpful for setting on/off switches for an undeterminate number of sources with undeterminate names.
  3. They also help toggling or setting a large or undeterminate number of switches with a single operation.
  4. They are helpful to store various preset configuration values for a large or undeterminate number of switches in single variables.

For example you can have presets like admin = 24; user = 17; poweruser = 31;. And just set config = admin and you are good to go rather than assigning many variables.

Another example, bit masks are used in Unity 3D engine to set the layers a Camera renders in runtime. Unity has two parts, an editor where you set things using GUI, and your code. Layer count and names are customized in the Unity editor. And in your camera code, instead of using a whole list of exposed layer objects, you can provide a bit mask to modify which layers are rendered with that particular camera.

Collapse
 
somedood profile image
Basti Ortiz

Yeah, I also have very limited experience with bitmasks myself. Seems like bitmasking isn't as impractical as I initially thought. Thanks for sharing!

Collapse
 
tsunwell profile image
tsunwell

I found your post long after it has been created, while I was looking for answer about bitwise operation.
I have a very good subject to use it.
I'm working on a personnal tool to play Soduku. It is a HTML, Javascript and CSS based tool.
In Sudoku game, the purpose is to complete a 9X9 cells grid, so the same digit exists only once in each row, column and 3X3 box. It is important to be able to control if a digit can be set at a specific place.
Classical control is quite complex, because you have to check every cell in the row, the column and the box hosting your target at least 3 loops with 9 steps each.
Using bits make things much easier : you just have to compute one value for each row, one for each column and one for each box of the grid, creating a binary digit corresponding (value 1) to the position of the possible digits. For example, if a row contains 3 an 8, the digit for that row is 101111011.
All 3 digits for a cell can be calculated in the same step of the loop.
For a cell, an simple AND on the three digits (row, column and box) to which belong the cell gives the possible values.
When a value is set, it is easy to update the corresponding digit in the 3 arrays, and then to check for next turns.
Thnaks for initial post and answers ...

Collapse
 
somedood profile image
Basti Ortiz • Edited

Honestly, I should be the one thanking you. What a brilliant use of bitmasks! Thanks for showing me this example. It really started my turning my gears for possible uses of bitmasking.

Collapse
 
bajix profile image
Thomas Sieverding

I absolutely love working with bitmasks! They pair incredibly well with MongoDB: there are query expressions for $bitsAllSet/$bitsAnySet + $bitsAllClear/$bitsAnyClear to make for some very succinct and expressive queries, and then there also $bit update operators for setting/unsetting bits. Field selection is a lot more straightforward; it indexes well, especially w/ compound indexes; and it just feels clean. I've found that a lot of queries are just much easier to express like this, for instance:

let cursor = Users.find({
  status: {
    $bitsAllSet: VERIFIED | FEATURED,
    $bitsAllClear: BANNED | DISABLED | PROVISIONAL
  }
}).lean().cursor();
Enter fullscreen mode Exit fullscreen mode

Also, there are a lot of times in which logical expressions are much cleaner with bitmasks. For instance, say you wanted to update a doc and wanted to trigger some behavior based off of the changes; the optimal way to do this IMO is to use a findOneAndUpdate with bitwise updates ($bit) and then to return the old doc / sync the bitwise updates and to compute the XOR. Once you have the XOR of the update, it's really easy to derive events: IE status & XOR & VERIFIED -> they are verified & they weren't verified before this update. The most powerful use case I've found is for shared documents, where you have a subdocument array for user data w/ an ObjectID ref + a bitmask, and then you can use $elemMatch updates to set each users respective flags, and then you can do awesome things like taking the AND of each users flags + the XOR of an update as to derive events based of when flags are mutually set by both users and became such as a result of this update. This very simple design pattern works for a staggering number of use cases.

Collapse
 
somedood profile image
Basti Ortiz

Ayyyyy. I can't disagree with your level of enthusiasm! Bitmasks are definitely great for these kinds of use cases. As long as the code for these design patterns are relatively straightforward and easy to fathom, bitmasks aren't really as "bad" and "esoteric" as I have made them to seem like in the article.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.