1. 程式人生 > >A definitive guide to conditional logic in JavaScript

A definitive guide to conditional logic in JavaScript

The Complete Truth Table

Caveats

There are two big caveats to treating JavaScript code like propositional logic: short circuiting and order of operations.

Short circuiting is something that JavaScript engines do to save time. Something that will not change the output of the whole expression is not evaluated. The function doSomething()

in the following examples is never called because, no matter what it returned, the outcome of the logical expression wouldn’t change:

// doSomething() is never calledfalse && doSomething();true || doSomething();

Recall that conjunctions (&&) are true only if both statements are true, and disjunctions (||

) are false only if both statements are false. In each of these cases, after reading the first value, no more calculations need to be done to evaluate the logical outcome of the expressions.

Because of this feature, JavaScript sometimes breaks logical commutativity. Logically A && B is equivalent to B && A

, but you would break your program if you commuted window && window.mightNotExist into window.mightNotExist && window. That’s not to say that the truthiness of a commuted expression is any different, just that JavaScript may throw an error trying to parse it.

The order of operations in JavaScript caught me by surprise because I was not taught that formal logic had an order of operations, other than by grouping and left-to-right. It turns out that many programming languages consider &&to have a higher precedence than ||. This means that && is grouped (not evaluated) first, left-to-right, and then || is grouped left-to-right. This means that A || B && C is not evaluated the same way as (A || B) && C, but rather as A || (B && C).

true || false && false; // evaluates to true(true || false) && false; // evaluates to false

Fortunately, grouping, (), holds the topmost precedence in JavaScript. We can avoid surprises and ambiguity by manually associating the statements we want evaluated together into discrete expressions. This is why many code linters prohibit having both && and || within the same group.

Calculating compound truth tables

Now that the truthiness of simple statements is known, the truthiness of more complex expressions can be calculated.

To begin, count the number of variables in the expression and write a truth table that has 2ⁿ rows.

Next, create a column for each of the variables and fill them with every possible combination of true/false values. I recommend filling the first half of the first column with T and the second half with F, then quartering the next column and so on until it looks like this:

Then write the expression down and solve it in layers, from the innermost groups outward for each combination of truth values:

As stated above, expressions which produce the same truth table can be substituted for each other.

Rules of replacements

Now I’ll cover several examples of rules of replacements that I often use. No truth tables are included below, but you can construct them yourself to prove that these rules are correct.

Double negation

Logically, A and !!A are equivalent. You can always remove a double negation or add a double negation to an expression without changing its truthiness. Adding a double-negation comes in handy when you want to negate part of a complex expression. The one caveat here is that in JavaScript !! also acts to coerce a value into a boolean, which may be an unwanted side-effect.

A === !!A

Commutation

Any disjunction (||), conjunction (&&), or bicondition (===) can swap the order of its parts. The following pairs are logically equivalent, but may change the program’s computation because of short-circuiting.

(A || B) === (B || A)(A && B) === (B && A)(A === B) === (B === A)

Association

Disjunctions and conjunctions are binary operations, meaning they only operate on two inputs. While they can be coded in longer chains — A || B || C || D — they are implicitly associated from left to right — ((A || B) || C) || D. The rule of association states that the order in which these groupings occur make no difference to the logical outcome.

((A || B) || C) === (A || (B || C))((A && B) && C) === (A && (B && C))

Distribution

Association does not work across both conjunctions and disjunctions. That is, (A && (B || C)) !== ((A && B) || C). In order to disassociate B and C in the previous example, you must distribute the conjunction — (A && B) || (A && C). This process also works in reverse. If you find a compound expression with a repeated disjunction or conjunction, you can un-distribute it, akin to factoring out a common factor in an algebraic expression.

(A && (B || C)) === ((A && B) || (A && C))(A || (B && C)) === ((A || B) && (A || C))

Another common occurrence of distribution is double-distribution (similar to FOIL in algebra):1. ((A || B) && (C || D)) === ((A || B) && C) || ((A || B) && D)2. ((A || B) && C) || ((A || B) && D) ===((A && C) || B && C)) || ((A && D) || (B && D))

(A || B) && (C || D) === (A && C) || B && C) || (A && D) || (B && D)(A && B) ||(C && D) === (A || C) && (B || C) && (A || D) && (B || D)

Material Implication

Implication expressions (A → B) typically get translated into code as if (A) { B } but that is not very useful if a compound expression has several implications in it. You would end up with nested if statements — a code smell. Instead, I often use the material implication rule of replacement, which says that A → B means either A is false or B is true.

(A → B) === (!A || B)

Tautology & Contradiction

Sometimes during the course of manipulating compound logical expressions, you’ll end up with a simple conjunction or disjunction that only involves one variable and its negation or a boolean literal. In those cases, the expression is either always true (a tautology) or always false (a contradiction) and can be replaced with the boolean literal in code.

(A || !A) === true(A || true) === true(A && !A) === false(A && false) === false

Related to these equivalencies are the disjunction and conjunction with the other boolean literal. These can be simplified to just the truthiness of the variable.

(A || false) === A(A && true) === A

Transposition

When manipulating an implication (A → B), a common mistake people make is to assume that negating the first part, A, implies the second part, B, is also negated — !A → !B. This is called the converse of the implication and it is not necessarily true. That is, having the original implication does not tell us if the converse is true because A is not a necessary condition of B. (If the converse is also true — for independent reasons — then A and B are biconditional.)

What we can know from the original implication, though, is that the contrapositive is true. Since B is a necessary condition for A (recall from the truth table for implication that if B is true, A must also be true), we can claim that !B → !A.

(A → B) === (!B → !A)

Material Equivalence

The name biconditional comes from the fact that it represents two conditional (implication) statements: A === B means that A → B and B → A. The truth values of A and B are locked into each other. This gives us the first material equivalence rule:

(A === B) === ((A → B) && (B → A))

Using material implication, double-distribution, contradiction, and commutation, we can manipulate this new expression into something easier to code:1. ((A → B) && (B → A)) === ((!A || B) && (!B || A))2. ((!A || B) && (!B || A)) === ((!A && !B) || (B && !B)) || ((!A && A) || (B && A))3. ((!A && !B) || (B && !B)) || ((!A && A) || (B && A)) === ((!A && !B) || (B && A))4. ((!A && !B) || (B && A)) === ((A && B) || (!A && !B))

(A === B) === ((A && B) || (!A && !B))

Exportation

Nested if statements, especially if there are no else parts, are a code smell. A simple nested if statement can be reduced into a single statement where the conditional is a conjunction of the two previous conditions:

if (A) {  if (B) {    C  }}// is equivalent toif (A && B) {  C}
(A → (B → C)) === ((A && B) → C)

DeMorgan’s Laws

DeMorgan’s Laws are essential to working with logical statements. They tell how to distribute a negation across a conjunction or disjunction. Consider the expression !(A || B). DeMorgan’s Laws say that when negating a disjunction or conjunction, negate each statement and change the && to ||or vice versa. Thus !(A || B) is the same as !A && !B. Similarly, !(A && B)is equivalent to !A || !B.

!(A || B) === !A && !B!(A && B) === !A || !B

Ternary (If-Then-Else)

Ternary statements (A ? B : C) occur regularly in programming, but they’re not quite implications. The translation from a ternary to formal logic is actually a conjunction of two implications, A → B and !A → C, which we can write as: (!A || B) && (A || C), using material implication.

(A ? B : C) === (!A || B) && (A || C)

XOR (Exclusive Or)

Exclusive Or, often abbreviated xor, means, “one or the other, but not both.” This differs from the normal or operator only in that both values cannot be true. This is often what we mean when we use “or” in plain English. JavaScript doesn’t have a native xor operator, so how would we represent this? 1. “A or B, but not both A and B”2. (A || B) && !(A && B) direct translation3. (A || B) && (!A || !B) DeMorgan’s Laws4. (!A || !B) && (A || B) commutativity5. A ? !B : B if-then-else definition

A ? !B : B is exclusive or (xor) in JavaScript

Alternatively,1. “A or B, but not both A and B”2. (A || B) && !(A && B) direct translation3. (A || B) && (!A || !B) DeMorgan’s Laws4. (A && !A) || (A && !B) || (B && !A) || (B && !B) double-distribution5. (A && !B) || (B && !A) contradiction replacement6. A === !B or A !== B material equivalence

A === !B or A !== Bis xor in JavaScript

Set Logic

So far we have been looking at statements about expressions involving two (or a few) values, but now we will turn our attention to sets of values. Much like how logical operators in compound expressions preserve truthiness in predictable ways, predicate functions on sets preserve truthiness in predictable ways.

A predicate function is a function whose input is a value from a set and whose output is a boolean. For the following code examples, I will use an array of numbers for a set and two predicate functions:isOdd = n => n % 2 !== 0; and isEven = n => n % 2 === 0;.

Universal Statements

A universal statement is one that applies to all elements in a set, meaning its predicate function returns true for every element. If the predicate returns false for any one (or more) element, then the universal statement is false. Array.prototype.every takes a predicate function and returns true only if every element of the array returns true for the predicate. It also terminates early (with false) if the predicate returns false, not running the predicate over any more elements of the array, so in practice avoid side-effects in predicates.

As an example, consider the array [2, 4, 6, 8], and the universal statement, “every element of the array is even.” Using isEven and JavaScript’s built-in universal function, we can run [2, 4, 6, 8].every(isEven) and find that this is true.

Array.prototype.every is JavaScript’s Universal Statement

Existential Statements

An existential statement makes a specific claim about a set: at least one element in the set returns true for the predicate function. If the predicate returns false for every element in the set, then the existential statement is false.

JavaScript also supplies a built-in existential statement: Array.prototype.some. Similar to every, some will return early (with true) if an element satisfies its predicate. As an example, [1, 3, 5].some(isOdd) will only run one iteration of the predicate isOdd (consuming 1 and returning true) and return true. [1, 3, 5].some(isEven) will return false.

Array.prototype.some is JavaScript’s Existential Statement

Universal Implication

Once you have checked a universal statement against a set, say nums.every(isOdd), it is tempting to think that you can grab an element from the set that satisfies the predicate. However, there is one catch: in Boolean logic, a true universal statement does not imply that the set is non-empty. Universal statements about empty sets are always true, so if you wish to grab an element from a set satisfying some condition, use an existential check instead. To prove this, run [].every(() => false). It will be true.

Universal statements about empty sets are always true.

Negating Universal and Existential Statements

Negating these statements can be surprising. The negation of a universal statement, say nums.every(isOdd), is not nums.every(isEven), but rather nums.some(isEven). This is an existential statement with the predicate negated. Similarly, the negation of an existential statement is a universal statement with the predicate negated.

!arr.every(el => fn(el)) === arr.some(el => !fn(el))!arr.some(el => fn(el)) === arr.every(el => !fn(el))

Set Intersections

Two sets can only be related to each other in a few ways, with regards to their elements. These relationships are easily diagrammed with Venn Diagrams, and can (mostly) be determined in code using combinations of universal and existential statements.

Two sets can each share some but not all of their elements, like a typical conjoined Venn Diagram:

A.some(el => B.includes(el)) && A.some(el => !B.includes(el)) && B.some(el => !A.includes(el)) describes a conjoined pair of sets

One set can contain all of the other set’s elements, but have elements not shared by the second set. This is a subset relationship, denoted as Subset ⊆ Superset.

B.every(el => A.includes(el)) describes the subset relationship B ⊆ A

The two sets can share no elements. These are disjoint sets.

A.every(el => !B.includes(el)) describes a disjoint pair of sets

Lastly, the two sets can share every element. That is, they are subsets of each other. These sets are equal. In formal logic, we would write A ⊆ B && B ⊆ A ⟷ A === B, but in JavaScript, there are some complications with this. In JavaScript, an Array is an ordered set and may contain duplicate values, so we cannot assume that the bidirectional subset code B.every(el => A.includes(el)) && A.every(el => B.includes(el)) implies the arrays A and B are equal. If A and B are Sets (meaning they were created with new Set()), then their values are unique and we can do the bidirectional subset check to see if A === B.

(A === B) === (Array.from(A).every(el => Array.from(B).includes(el)) && Array.from(B).every(el => Array.from(A).includes(el)), given that A and Bare constructed using new Set()

Translating Logic to English

This section is probably the most useful in the article. Here, now that you know the logical operators, their truth tables, and rules of replacement, you can learn how to translate an English phrase into code and simplify it. In learning this translation skill, you will also be able to read code better, storing complex logic in simple phrases in your mind.

Below is a table of logical code (left) and their English equivalents (right) that was heavily borrowed from the excellent book, Essentials of Logic.