When I get confused about a complex condition in a programming language, I tend to use logical identities from maths to understand or simplify the condition.
By "complex condition", I mean something like:
if (!a && (b || a)) {
// ...
}
And by "logical identities", I'm referring to the rules of Boolean algebra that tell us which two logical expressions are equivalent and thus interchangeable. These rules apply to some extent to the NOT, AND and OR operations in many programming languages, since those operations are based on Boolean algebra.
In this article, I'll explain how I use these identities in my day-to-day programming. I'll focus mostly on JavaScript here, but the gist should apply to other languages too.
I'll start with the associative property. JavaScript's &&
and ||
are associative, just like AND and OR are in maths, so the following expressions are equivalent:
(a && b) && c
a && (b && c)
a && b && c
Replace &&
with ||
in the three expressions and they'll still be equivalent.
My takeaway from this identity is that the brackets are redundant, so I usually omit them, because when overused they reduce readability. I've seen code like this that overused brackets:
if (
((firstCondition && secondCondition) && thirdCondition) &&
(fourthCondition && fifthCondition)
) {
// ...
}
The above could very well be written as:
if (
firstCondition &&
secondCondition &&
thirdCondition &&
fourthCondition &&
fifthCondition
) {
// ...
}
Another identity is the distributive property. I used this recently to simplify an SQL query. I initially wrote something like:
select ...
where
title like '%Child%'
or (title not like '%Child%' and ticketstatus = 'completed')
but the adjacent title like '%Child%'
and title not like '%Child%'
felt a bit off. So I thought I'd try simplifying the condition using logical identities, as I'll now demonstrate.
For brevity, let's use a
to refer to the condition title like '%Child%'
and b
to refer to ticketstatus = 'completed'
. We can then write title not like '%Child%'
as not a
and the full condition (from the query above) as a or (not a and b)
The OR operation distributes over the AND operation (and vice versa too), so we can expand the full condition into (a or not a) and (a or b)
.
(a or not a)
is always true, because one of a
and not a
must be true. So (a or not a) and (a or b)
becomes true and (a or b)
, which in turn becomes just a or b
(can you tell why?).
Substituting a or b
back into the original SQL query gives us an easier-to-read query:
select ...
where
title like '%Child%'
or ticketstatus = 'completed'
It's worth noting that this kind of substitution should be done carefully, as it can change the meaning of a program. To demonstrate this, I'll express the original and simplified conditions as follows in JavaScript:
const original =
like(title, "Child") ||
(!like(title, "Child") && equals(ticketstatus, "completed"));
const simplified =
like(title, "Child") ||
equals(ticketstatus, "completed");
function like(a, b) {
console.log("running like");
return a.includes(b);
}
function equals(a, b) {
console.log("running equals");
return a === b;
}
(It's contrived, I know, but it helps to prove my point.)
Our identities tell us that the right-hand-side expressions of original
and simplified
are interchangeable. But are they really?
It's true that both expressions always yield the same value, so that original === simplified
, regardless of the values of title
and ticketstatus
. But this doesn't mean they're interchangeable. In fact, they aren't, because of their different side effects.
Suppose title
has the value "Test ticket"
and ticketstatus
has "completed"
. The original expression will log the following to the console:
running like
running like
running equals
While the simplified will log:
running like
running equals
As you can see, there is a difference, so the expressions are not interchangeable.
While I'm just using logs in this example, the side effects could be something more significant, like updating a non-local variable or writing to a file. So, substitute with care when you have side effects. (That said, some identities like associativity still hold even when side effects are involved.)
I'll now move on to De Morgan's laws. The laws go like this:
!(a && b) === !a || !b
!(a || b) === !a && !b
I often use these identities when interpreting conditions.
Let's say I'm working with a list of containers, where each container is an object having two nullable properties, capacity
and volume
. If you're familiar with TypeScript, the type of each container would look like this:
type Container = {
capacity: number | null
volume: number | null
}
Now suppose I want to filter the list to get only containers whose capacity and volume are both known. Here's one way I may go about it:
const result = containers.filter(
(c) => c.capacity !== null && c.volume != null
)
I'd read the above as "give me containers whose capacity is not null and volume is not null", but the meaning may or may not be immediately clear to me. To confirm what I've written is indeed what I intended, I would then express the condition differently (in my mind at least) using De Morgan's laws:
const result = containers.filter(
(c) => !(c.capacity === null || c.volume === null)
)
And I'd interpret this as "give me all containers except those with null capacity or null volume". If this alternative condition still makes sense—and it does in this case—then I know my previous condition was correct. Otherwise I'd have to fix my previous condition.
One final identity, which I recently learnt is called the exportation rule, is that a nested if
statement like:
if (a) {
if (b) {
// ...
}
}
can be reduced to:
if (a && b) {
// ...
}
provided there's no code between the inner and outer if
statements.
To be fair, the exportation rule doesn't exactly say this. What it says is that the following two are equivalent:
- IF a THEN (IF b THEN c)
- IF a AND b THEN c
This "IF…THEN" operation (also known as implication) isn't quite the same as JavaScript's if
statement. For one thing, IF…THEN yields a value, while if
doesn't. Nonetheless, the analogy works for this example of nested if
s.
The identities I've discussed so far are my most frequently used, but others exist too, as you may imagine. You can google "logical identities" to find them. Some of them apply in JS (and programming broadly), while some don't, so you'll need to verify them before taking them for granted.
As a parting exercise, try simplifying the "complex condition" I gave at the beginning of this article:
if (!a && (b || a)) {
// ...
}
Hope you found this useful :)
Thank you Yusuf Atolagbe for reviewing drafts of this post.