Eine definitive Anleitung zur bedingten Logik in JavaScript

Ich bin Front-End-Ingenieur und Mathematiker. Ich verlasse mich täglich auf meine mathematische Ausbildung beim Schreiben von Code. Ich verwende keine Statistiken oder Berechnungen, sondern mein gründliches Verständnis der Booleschen Logik. Oft habe ich eine komplexe Kombination aus kaufmännischem Und, Pfeifen, Ausrufezeichen und Gleichheitszeichen in etwas Einfacheres und viel Lesbareres verwandelt. Ich möchte dieses Wissen teilen, deshalb habe ich diesen Artikel geschrieben. Es ist lang, aber ich hoffe, es ist für Sie genauso vorteilhaft wie für mich. Genießen!

Truthy & Falsy-Werte in JavaScript

Bevor wir logische Ausdrücke studieren, sollten wir verstehen, was in JavaScript „wahr“ ist. Da JavaScript lose typisiert ist, werden Werte in logischen Ausdrücken in Boolesche Werte umgewandelt. ifAussagen, &&, ||, und ternäre Bedingungen alle nötigen Werte in booleans. Beachten Sie, dass dies nicht bedeutet, dass sie immer einen Booleschen Wert aus der Operation zurückgeben.

Es gibt nur sechs falsy Werte in JavaScript - false, null, undefined, NaN, 0, und ""- und alles andere ist truthy . Dies bedeutet, dass []und {}beide wahr sind, die dazu neigen, Menschen zu stolpern.

Die logischen Operatoren

In der formalen Logik existieren nur wenige Operatoren: Negation, Konjunktion, Disjunktion, Implikation und Bicondition. Jeder von ihnen hat einen JavaScript - Äquivalent: !, &&, ||, if (/* condition */) { /* then consequence */}, und ===, respectively. Diese Operatoren erstellen alle anderen logischen Anweisungen.

Wahrheitstabellen

Schauen wir uns zunächst die Wahrheitstabellen für jeden unserer Basisoperatoren an. Eine Wahrheitstabelle sagt uns, was die Wahrhaftigkeit eines Ausdrucks auf der Wahrhaftigkeit seiner Teile beruht . Wahrheitstabellen sind wichtig. Wenn zwei Ausdrücke dieselbe Wahrheitstabelle erzeugen, sind diese Ausdrücke äquivalent und können sich gegenseitig ersetzen .

Die Negationstabelle ist sehr einfach. Negation ist der einzige unäre logische Operator, der nur auf einen einzelnen Eingang wirkt. Dies bedeutet, dass dies !A || Bnicht dasselbe ist wie !(A || B). Klammern verhalten sich wie die Gruppierungsnotation, die Sie in der Mathematik finden.

Zum Beispiel sollte die erste Zeile in der Negation-Wahrheitstabelle (unten) folgendermaßen gelesen werden: "Wenn Aussage A wahr ist, dann ist der Ausdruck! A falsch."

Eine einfache Aussage zu negieren ist nicht schwierig. Die Negation von "es regnet" ist "es regnet nicht ", und die Negation des Grundelements von JavaScript trueist natürlich false. Das Negieren komplexer Aussagen oder Ausdrücke ist jedoch nicht so einfach. Was ist die Negation von "es regnet immer " oder isFoo && isBar?

Die Konjunktionstabelle zeigt, dass der Ausdruck A && Bnur dann wahr ist, wenn sowohl A als auch B wahr sind. Dies sollte aus dem Schreiben von JavaScript sehr vertraut sein.

Die Disjunktionstabelle sollte ebenfalls sehr vertraut sein. Eine Disjunktion (logische ODER-Anweisung) ist wahr, wenn eine oder beidevon A und B sind wahr.

Die Implikationstabelle ist nicht so vertraut. Da A B impliziert , impliziert A, dass B wahr ist, dass B wahr ist. B kann jedoch aus anderen Gründen als A wahr sein, weshalb die letzten beiden Zeilen der Tabelle wahr sind. Die einzige Zeitimplikation ist falsch, wenn A wahr ist und B falsch ist, weil dann A nicht B impliziert.

Während ifAnweisungen für Implikationen in JavaScript verwendet werden, iffunktionieren nicht alle Anweisungen auf diese Weise. Normalerweise verwenden wir ifals Flusskontrolle, nicht als Wahrheitsprüfung, bei der die Konsequenz auch bei der Prüfung eine Rolle spielt. Hier ist die archetypische Implikationsaussageif :

function implication(A, B) { if (A) { return B; } else { /* if A is false, the implication is true */ return true; }}

Mach dir keine Sorgen, dass dies etwas umständlich ist. Es gibt einfachere Möglichkeiten, Implikationen zu codieren. Aufgrund dieser Unbeholfenheit werde ich diesen Artikel weiterhin als Symbol für Implikationen verwenden.

Der Bicondition- Operator, manchmal auch als if-and-only-if (IFF) bezeichnet, wird nur dann als wahr ausgewertet, wenn die beiden Operanden A und B denselben Wahrheitswert haben. Aufgrund der Art und Weise, wie JavaScript Vergleiche verarbeitet, sollte die Verwendung von ===für logische Zwecke nur für Operanden verwendet werden, die in Boolesche Werte umgewandelt wurden. Das heißt, stattdessen A === Bsollten wir verwenden !!A === !!B.

Vorsichtsmaßnahmen

Es gibt zwei große Einschränkungen bei der Behandlung von JavaScript-Code wie Aussagenlogik: Kurzschluss und Reihenfolge der Operationen .

Kurzschlüsse sind etwas, was JavaScript-Engines tun, um Zeit zu sparen. Etwas, das die Ausgabe des gesamten Ausdrucks nicht ändert, wird nicht ausgewertet. Die Funktion doSomething()in den folgenden Beispielen wird niemals aufgerufen, da sich das Ergebnis des logischen Ausdrucks unabhängig von der Rückgabe nicht ändern würde:

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

Denken Sie daran, dass Konjunktionen ( &&) nur dann wahr sind, wenn beide Aussagen wahr sind , und Disjunktionen ( ||) nur dann falsch sind , wenn beide Aussagen falsch sind. In jedem dieser Fälle müssen nach dem Lesen des ersten Werts keine weiteren Berechnungen durchgeführt werden, um das logische Ergebnis der Ausdrücke zu bewerten.

Aufgrund dieser Funktion unterbricht JavaScript manchmal die logische Kommutativität. Logischer A && Bentspricht B && A, aber Sie würden Ihr Programm brechen , wenn Sie pendelte window && window.mightNotExistin window.mightNotExist && window. Das ist nicht zu sagen , dass die Truthiness eines vertauschtem Ausdruck ist anders, nur dass JavaScript kann einen Fehler aus , versuchen , es zu analysieren.

Die Reihenfolge der Operationen in JavaScript überraschte mich , weil ich nicht , dass die formale Logik gelehrt hatte eine Reihenfolge von Operationen, die nicht durch die Gruppierung und von links nach rechts. Es stellt sich heraus, dass viele Programmiersprachen &&eine höhere Priorität als haben ||. Dies bedeutet, dass &&zuerst von links nach rechts gruppiert (nicht ausgewertet) und dann von ||links nach rechts gruppiert wird. Dies bedeutet , dass A || B && Cwird nicht ausgewertet , um die gleiche Art und Weise wie (A || B) && C, sondern als A || (B && C).

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

Glücklicherweise Gruppierung , ()hält die oberste Priorität in JavaScript. Wir können Überraschungen und Mehrdeutigkeiten vermeiden, indem wir die Aussagen, die ausgewertet werden sollen, manuell zu diskreten Ausdrücken zusammenfügen. Aus diesem Grund verbieten viele Code-Linters, beide &&und ||innerhalb derselben Gruppe zu haben.

Berechnung zusammengesetzter Wahrheitstabellen

Nachdem die Wahrhaftigkeit einfacher Aussagen bekannt ist, kann die Wahrhaftigkeit komplexerer Ausdrücke berechnet werden.

Zählen Sie zunächst die Anzahl der Variablen im Ausdruck und schreiben Sie eine Wahrheitstabelle mit 2ⁿ Zeilen.

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 Bis 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 → BandB → 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 translation

3. (A || B) && (!A || !B)DeMorgan’s Laws

4. (!A || !B) && (A || B)commutativity

5. A ? !B : BWenn-Dann-Sonst-Definition

A ? !B : B ist exklusiv oder (xor) in JavaScript

Alternative,

1. "A oder B, aber nicht sowohl A als auch B"

2. (A || B) && !(A && B)direkte Übersetzung

3. (A || B) && (!A || !B)DeMorgans Gesetze

4. (A && !A) || (A && !B) || (B && !A) || (B && !B)Doppelverteilung

5. (A && !B) || (B && !A)Widerspruch Ersatz

6. A === !Boder A !== Bmaterielle Äquivalenz

A === !BoderA !== B ist xor in JavaScript

Logik einstellen

Bisher haben wir uns Aussagen über Ausdrücke angesehen, die zwei (oder einige) Werte betreffen, aber jetzt werden wir unsere Aufmerksamkeit auf Wertesätze richten. Ähnlich wie logische Operatoren in zusammengesetzten Ausdrücken die Wahrhaftigkeit auf vorhersehbare Weise bewahren, bewahren Prädikatfunktionen auf Mengen die Wahrhaftigkeit auf vorhersehbare Weise.

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.

Betrachten Sie als Beispiel das Array [2, 4, 6, 8]und die universelle Aussage: "Jedes Element des Arrays ist gerade." Mit der in isEvenJavaScript integrierten Universalfunktion können wir ausführen [2, 4, 6, 8].every(isEven)und feststellen, dass dies der Fall ist true.

Array.prototype.every ist die universelle Anweisung von JavaScript

Existenzaussagen

Eine existenzielle Anweisung erhebt einen bestimmten Anspruch auf eine Menge: Mindestens ein Element in der Menge gibt true für die Prädikatfunktion zurück. Wenn das Prädikat für jedes Element in der Menge false zurückgibt, ist die existenzielle Anweisung false.

JavaScript liefert auch eine integrierte existenzielle Anweisung : Array.prototype.some. Ähnlich every, somewird wieder früh (mit true) , wenn ein Element erfüllt sein Prädikat. Als Beispiel [1, 3, 5].some(isOdd)wird nur eine Iteration des Prädikats isOdd(Konsumieren 1und Zurückgeben true) ausgeführt und zurückgegeben true. [1, 3, 5].some(isEven)wird zurückkehren false.

Array.prototype.some ist die existenzielle Anweisung von JavaScript

Universelle Implikation

Wenn Sie beispielsweise eine universelle Aussage mit einer Menge verglichen haben nums.every(isOdd), ist es verlockend zu glauben, dass Sie ein Element aus der Menge greifen können, das das Prädikat erfüllt. Es gibt jedoch einen Haken: In der Booleschen Logik bedeutet eine echte universelle Aussage nicht, dass die Menge nicht leer ist. Universelle Aussagen über leere Mengen sind immer wahr . Wenn Sie also ein Element aus einer Menge abrufen möchten, das eine bestimmte Bedingung erfüllt, verwenden Sie stattdessen eine existenzielle Prüfung. Um dies zu beweisen, führen Sie [].every(() => false) aus. Es wird wahr sein.

Universelle Aussagen über leere Mengen sind immer wahr .

Universelle und existenzielle Aussagen negieren

Das Negieren dieser Aussagen kann überraschend sein. Die Negation einer universellen Aussage nums.every(isOdd)ist beispielsweise nicht nums.every(isEven), sondern nums.some(isEven). Dies ist eine existenzielle Aussage, bei der das Prädikat negiert wird. Ebenso ist die Negation einer existenziellen Aussage eine universelle Aussage, bei der das Prädikat negiert wird.

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

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

Schnittpunkte einstellen

Zwei Mengen können in Bezug auf ihre Elemente nur auf wenige Arten miteinander in Beziehung gesetzt werden. Diese Beziehungen lassen sich mit Venn-Diagrammen leicht grafisch darstellen und können (meistens) im Code mithilfe von Kombinationen aus universellen und existenziellen Anweisungen bestimmt werden.

Zwei Sets können jeweils einige, aber nicht alle Elemente gemeinsam nutzen, wie ein typisches verbundenes Venn-Diagramm:

A.some(el => B.includes(el)) && A.some(el => !B.includes(el)) && B.some(el => !A.includes (el)) beschreibt ein verbundenes Paar von Mengen

Ein Satz kann alle Elemente des anderen Satzes enthalten, jedoch Elemente, die vom zweiten Satz nicht gemeinsam genutzt werden. Dies ist eine Teilmengenbeziehung , die als bezeichnet wird Subset ⊆ Superset.

B.every(el => A.includes(el)) beschreibt die Teilmengenbeziehung B ⊆ A.

Die beiden Sätze können keine Elemente gemeinsam nutzen. Dies sind disjunkte Mengen.

A.every(el => !B.includes(el)) beschreibt ein disjunktes Satzpaar

Schließlich können die beiden Sätze jedes Element gemeinsam nutzen. Das heißt, sie sind Teilmengen voneinander. Diese Sätze sind gleich . In der formalen Logik würden wir schreiben A ⊆ B && B ⊆ A ⟷ A === B, aber in JavaScript gibt es einige Komplikationen. In JavaScript Arrayist an eine geordnete Menge und kann doppelte Werte enthalten. Daher können wir nicht davon ausgehen, dass der bidirektionale Teilmengencode B.every(el => A.includes(el)) && A.every(el => B.includes (el) impliziert, dass die a- rStrahlen Aund B gleich sind l. Wenn Aund B Mengen sind (was bedeutet, dass sie with newSet () erstellt wurden), sind ihre Werte eindeutig und wir können die bidirektionale Teilmengenprüfung für s ee if A=== B durchführen.

(A === B) === (Array.from(A).every(el => Array.from(B).includes(el)) && Array.from(B).every(el => Array.from(A).includes (el)), vorausgesetzt, dass Aund Bare konstruierte using newSet ()

Logik ins Englische übersetzen

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.

Below, I will go through some real-world examples from my own work where I interpret from English to code, and vice-versa, and simplify code with the rules of replacement.

Example 1

Recently, to satisfy the EU’s GDPR requirements, I had to create a modal that showed my company’s cookie policy and allowed the user to set their preferences. To make this as unobtrusive as possible, we had the following requirements (in order of precedence):

  1. If the user wasn’t in the EU, never show the GDPR preferences modal.
  2. 2. If the app programmatically needs to show the modal (if a user action requires more permission than currently allowed), show the modal.
  3. If the user is allowed to have the less-obtrusive GDPR banner, do not show the modal.
  4. If the user has not already set their preferences (ironically saved in a cookie), show the modal.

I started off with a series of if statements modeled directly after these requirements:

const isGdprPreferencesModalOpen = ({ shouldModalBeOpen, hasCookie, hasGdprBanner, needsPermissions}) => { if (!needsPermissions) { return false; } if (shouldModalBeOpen) { return true; } if (hasGdprBanner) { return false; } if (!hasCookie) { return true; } return false;}

To be clear, the above code works, but returning boolean literals is a code smell. So I went through the following steps:

/* change to a single return, if-else-if structure */let result;if (!needsPermissions) { result = false;} else if (shouldBeOpen) { result = true;} else if (hasBanner) { result = false;} else if (!hasCookie) { result = true} else { result = false;}return result;
/* use the definition of ternary to convert to a single return */return !needsPermissions ? false : (shouldBeOpen ? true : (hasBanner ? false : (!hasCookie ? true : false)))
/* convert from ternaries to conjunctions of disjunctions */return (!!needsPermissions || false) && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || ((!hasBanner || false) && (hasBanner || !hasCookie))))
/* simplify double-negations and conjunctions/disjunctions with boolean literals */return needsPermissions && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || (!hasBanner && (hasBanner || !hasCookie))))
/* DeMorgan's Laws */return needsPermissions && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || ((!hasBanner && hasBanner) || (hasBanner && !hasCookie))))
/* eliminate tautologies and contradictions, simplify */return needsPermissions && (!needsPermissions || (shouldBeOpen || (hasBanner && !hasCookie)))
/* DeMorgan's Laws */return (needsPermissions && !needsPermissions) || (needsPermissions && (shouldBeOpen || (hasBanner && !hasCookie)))
/* eliminate contradiction, simplify */return needsPermissions && (shouldBeOpen || (hasBanner && !hasCookie))

I ended up with something that I think is more elegant and still readable:

const isGdprPreferencesModalOpen = ({ needsPermissions, shouldBeOpen, hasBanner, hasCookie,}) => ( needsPermissions && (shouldBeOpen || (!hasBanner && !hasCookie)));

Example 2

I found the following code (written by a coworker) while updating a component. Again, I felt the urge to eliminate the boolean literal returns, so I refactored it.

const isButtonDisabled = (isRequestInFlight, state) => { if (isRequestInFlight) { return true; } if (enabledStates.includes(state)) { return false; } return true;};

Sometimes I do the following steps in my head or on scratch paper, but most often, I write each next step in the code and then delete the previous step.

// convert to if-else-if structurelet result;if (isRequestInFlight) { result = true;} else if (enabledStates.includes(state)) { result = false;} else { result = true;}return result;
// convert to ternaryreturn isRequestInFlight ? true : enabledStates.includes(state) ? false : true;
/* convert from ternary to conjunction of disjunctions */return (!isRequestInFlight || true) && (isRequestInFlight || ((!enabledStates.includes(state) || false) && (enabledStates.includes(state) || true))
/* remove tautologies and contradictions, simplify */return isRequestInFlight || !enabledStates.includes(state)

Then I end up with:

const isButtonDisabled = (isRequestInFlight, state) => ( isRequestInFlight || !enabledStates.includes(state));

In this example, I didn’t start with English phrases and I never bothered to interpret the code to English while doing the manipulations, but now, at the end, I can easily translate this: “the button is disabled if either the request is in flight or the state is not in the set of enabled states.” That makes sense. If you ever translate your work back to English and it doesn’t make sense, re-check your work. This happens to me often.

Example 3

While writing an A/B testing framework for my company, we had two master lists of Enabled and Disabled experiments and we wanted to check that every experiment (each a separate file in a folder) was recorded in one or the other list but not both. This means the enabled and disabled sets are disjointed and the set of all experiments is a subset of the conjunction of the two sets of experiments. The reason the set of all experiments must be a subset of the combination of the two lists is that there should not be a single experiment that exists outside the two lists.

const isDisjoint = !enabled.some(el => disabled.includes(el)) && !disabled.some(el => enabled.includes(el));const isSubset = allExperiments.every( el => enabled.concat(disabled).includes(el));assert(isDisjoint && isSubset);

Conclusion

Hopefully this has all been helpful. Not only are the skills of translating between English and code useful, but having the terminology to discuss different relationships (like conjunctions and implications) and the tools to evaluate them (truth tables) is handy.