En endelig guide til betinget logik i JavaScript

Jeg er en front-end ingeniør og matematiker. Jeg stoler på min matematiske træning dagligt i at skrive kode. Det er ikke statistik eller beregning, jeg bruger, men snarere min grundige forståelse af boolsk logik. Ofte har jeg forvandlet en kompleks kombination af tegn, rør, udråbstegn og ligestillede tegn til noget enklere og meget mere læsbart. Jeg vil gerne dele denne viden, så jeg skrev denne artikel. Det er længe, ​​men jeg håber, det er lige så gavnligt for dig som det har været for mig. God fornøjelse!

Sandheds- og falske værdier i JavaScript

Før vi studerer logiske udtryk, lad os forstå, hvad der er "sandt" i JavaScript. Da JavaScript er skrevet løst, tvinger den værdier til booleanske i logiske udtryk. ifudsagn, &&, ||, og ternære betingelser alle tvinge værdier i boolesk type. Bemærk, at dette ikke betyder, at de altid returnerer en boolean fra operationen.

Der er kun seks falsy værdier i JavaScript - false, null, undefined, NaN, 0, og ""- og alt andet er truthy . Dette betyder, at []og {}begge er sandfærdige, som har tendens til at rejse folk op.

De logiske operatører

I formel logik findes der kun få operatører: negation, konjunktion, disjunktion, implikation og bicondition. Hver af disse har en JavaScript tilsvarende: !, &&, ||, if (/* condition */) { /* then consequence */}, og ===hhv. Disse operatører opretter alle andre logiske udsagn.

Sandhedstabeller

Lad os først se på sandhedstabellerne for hver af vores grundlæggende operatører. En sandhed tabellen fortæller os, hvad det truthiness af et udtryk er baseret på den truthiness af dens dele . Sandhedstabeller er vigtige. Hvis to udtryk genererer den samme sandhedstabel, så er disse udtryk ækvivalente og kan erstatte hinanden .

Den Negation bordet er meget ligetil. Negation er den eneste unære logiske operatør, der kun handler på en enkelt input. Dette betyder, at !A || Bdet ikke er det samme som !(A || B). Parenteser fungerer som den grupperingsnotation, du finder i matematik.

For eksempel skal den første række i Negation-sandhedstabellen (nedenfor) læses således: "hvis udsagn A er sandt, så er udtrykket! A falsk."

At negere en simpel erklæring er ikke svært. Negationen af ​​"det regner" er "det regner ikke ", og negationen af ​​JavaScript's primitive trueer selvfølgelig false. At negere komplekse udsagn eller udtryk er imidlertid ikke så simpelt. Hvad er negationen af ​​"det regner altid " eller isFoo && isBar?

De Konjunktion tabellen viser, at udtrykket A && Ber sandt, hvis både A og B er sande. Dette burde være meget velkendt, når man skriver JavaScript.

Den disjunktion Tabellen bør også være meget fortrolig. En adskillelse (logisk ELLER udsagn) er sand, hvis en eller begge deleaf A og B er sande.

Den Følgevirkning Tabellen er ikke så velkendt. Da A antyder B, betyder A at være sandt, at B er sandt. B kan dog være sandt af andre grunde end A, hvorfor de sidste to linjer i tabellen er sande. Den eneste gang implikation er falsk, er når A er sand og B er falsk, for så betyder A ikke B.

Mens ifudsagn bruges til implikationer i JavaScript, iffungerer ikke alle udsagn på denne måde. Normalt bruger vi ifsom en flowkontrol, ikke som en sandhedskontrol, hvor konsekvensen også betyder noget i kontrollen. Her er den arketypiske implikationserklæringif :

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

Bare rolig, at dette er noget akavet. Der er lettere måder at kode implikationer på. På grund af denne akavethed vil jeg dog fortsætte med at bruge som symbol for implikationer i hele denne artikel.

Den Bicondition operatør, også kaldet hvis-and-only-if (IFF), evalueres til sand, hvis de to operander, A og B, har samme truthiness værdi. På grund af hvordan JavaScript håndterer sammenligninger, bør brugen af ===til logiske formål kun bruges på operander, der er kastet til booleanske. Det er i stedet for A === B, vi skal bruge !!A === !!B.

Advarsler

Der er to store forbehold for behandling af JavaScript-kode som propositionelogik: kortslutning og rækkefølge for operationer .

Kortslutning er noget, som JavaScript-motorer gør for at spare tid. Noget, der ikke vil ændre output for hele udtrykket, evalueres ikke. Funktionen doSomething()i de følgende eksempler kaldes aldrig, fordi uanset hvad den returnerede, ville resultatet af det logiske udtryk ikke ændre sig:

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

Husk, at konjunktioner ( &&) kun er sande , hvis begge udsagn er sande , og adskillelser ( ||) kun er falske , hvis begge udsagn er falske. I hver af disse tilfælde, efter at have læst den første værdi, er der ikke behov for flere beregninger for at evaluere det logiske resultat af udtrykkene.

På grund af denne funktion bryder JavaScript undertiden den logiske kommutativitet. Logisk A && Bsvarer til B && A, men du ville bryde dit program, hvis du pendlede window && window.mightNotExistind window.mightNotExist && window. Det er ikke at sige, at sandheden af et pendlet udtryk er anderledes, bare at JavaScript kan kaste en fejl, der forsøger at analysere det.

Rækkefølgen af ​​operationer i JavaScript overraskede mig, fordi jeg ikke fik at vide, at formel logik havde en rækkefølge af operationer, bortset fra ved gruppering og fra venstre mod højre. Det viser sig, at mange programmeringssprog anser for &&at have en højere prioritet end ||. Dette betyder, at den &&er grupperet (ikke evalueret) først, fra venstre mod højre og derefter ||er grupperet fra venstre mod højre. Dette betyder, at A || B && Cder ikke evalueres på samme måde som (A || B) && C, men snarere som A || (B && C).

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

Heldigvis gruppering , ()besidder den øverste prioritet i JavaScript. Vi kan undgå overraskelser og tvetydighed ved manuelt at knytte de udsagn, vi ønsker evalueret sammen, til diskrete udtryk. Dette er grunden til, at mange kodeforbindelser forbyder at have både &&og ||inden for samme gruppe.

Beregning af sammensatte sandhedstabeller

Nu hvor sandheden ved enkle udsagn er kendt, kan sandheden af ​​mere komplekse udtryk beregnes.

Til at begynde med skal du tælle antallet af variabler i udtrykket og skrive en sandhedstabel, der har 2ⁿ rækker.

Opret derefter en kolonne for hver af variablerne og udfyld dem med alle mulige kombinationer af sande / falske værdier. Jeg anbefaler at udfylde den første halvdel af den første kolonne med Tog den anden halvdel med F, derefter kvartere den næste kolonne og så videre, indtil den ser sådan ud:

Skriv derefter udtrykket ned, og løs det i lag, fra de inderste grupper udad for hver kombination af sandhedsværdier:

Som nævnt ovenfor kan udtryk, der producerer den samme sandhedstabel, erstattes af hinanden.

Regler for udskiftning

Nu vil jeg dække flere eksempler på udskiftningsregler, som jeg ofte bruger. Ingen sandhedstabeller er inkluderet nedenfor, men du kan selv konstruere dem for at bevise, at disse regler er korrekte.

Dobbelt 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 : Bhvis-så-ellers-definition

A ? !B : B er eksklusiv eller (xor) i JavaScript

Alternativt

1. “A eller B, men ikke både A og B”

2. (A || B) && !(A && B)direkte oversættelse

3. (A || B) && (!A || !B)DeMorgan's love

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

5. (A && !B) || (B && !A)udskiftning af modsigelse

6. A === !Beller A !== Bmateriel ækvivalens

A === !B ellerA !== B er xor i JavaScript

Indstil logik

Indtil videre har vi set på udsagn om udtryk, der involverer to (eller få) værdier, men nu vil vi rette opmærksomheden mod sæt af værdier. Ligesom hvordan logiske operatorer i sammensatte udtryk bevarer sandhed på forudsigelige måder, bevarer predikatfunktioner på sæt sandhed på forudsigelige måder.

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.

Overvej som eksempel matrixen [2, 4, 6, 8]og den universelle sætning, "hvert element i arrayet er jævnt." Ved hjælp af isEvenog JavaScript's indbyggede universelle funktion kan vi køre [2, 4, 6, 8].every(isEven)og finde ud af, at dette er true.

Array.prototype.every er JavaScript's universelle erklæring

Eksistentielle udsagn

En eksistentiel erklæring fremsætter et specifikt krav om et sæt: mindst et element i sættet returnerer sandt for predikatfunktionen. Hvis prædikatet returnerer falsk for hvert element i sættet, er det eksistentielle udsagn falsk.

JavaScript leverer også en indbygget eksistentiel erklæring: Array.prototype.some. Svarende til every, somevil vende tilbage tidligt (med sand), hvis et element opfylder dens prædikat. Kører som et eksempel [1, 3, 5].some(isOdd)kun en iteration af prædikatet isOdd(forbruger 1og returnerer true) og returnerer true. [1, 3, 5].some(isEven)vender tilbage false.

Array.prototype.some er JavaScript's eksistenserklæring

Universel implikation

Når du først har kontrolleret en universel udsagn mod et sæt, nums.every(isOdd)er det fristende at tro, at du kan få fat i et element fra sættet, der tilfredsstiller prædikatet. Der er dog en fangst: i boolsk logik betyder en ægte universel erklæring ikke, at sættet ikke er tomt. Universelle udsagn om tomme sæt er altid sande , så hvis du ønsker at få fat i et element fra et sæt, der opfylder en eller anden betingelse, skal du i stedet bruge en eksistentiel kontrol. For at bevise dette skal du køre [].every(() => false). Det vil være sandt.

Universelle udsagn om tomme sæt er altid sande .

Negation af universelle og eksistentielle udsagn

At negere disse udsagn kan være overraskende. Negationen af ​​en universel erklæring er f.eks nums.every(isOdd). Ikke nums.every(isEven), men snarere nums.some(isEven). Dette er en eksistentiel erklæring med prædikatet negeret. Tilsvarende er negationen af ​​en eksistentiel erklæring en universel erklæring med predikatet negeret.

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

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

Indstil kryds

To sæt kan kun relateres til hinanden på få måder med hensyn til deres elementer. Disse forhold er let skematiseret med Venn-diagrammer og kan (for det meste) bestemmes i kode ved hjælp af kombinationer af universelle og eksistentielle udsagn.

To sæt kan hver dele nogle, men ikke alle deres elementer, som et typisk sammenføjet Venn-diagram:

A.some(el => B.includes(el)) && A.some(el => !B.includes(el)) && B.some(el => !A.includes (el)) beskriver et sammensat par sæt

Et sæt kan indeholde alle de andre sets elementer, men har elementer, der ikke deles af det andet sæt. Dette er et delmængdeforhold , betegnet som Subset ⊆ Superset.

B.every(el => A.includes(el)) beskriver delmængdeforholdet B ⊆ A

De to sæt kan ikke dele nogen elementer. Disse er usammenhængende sæt.

A.every(el => !B.includes(el)) beskriver et uensartet sæt sæt

Endelig kan de to sæt dele hvert element. Det vil sige, de er delmængder af hinanden. Disse sæt er ens . I formel logik ville vi skrive A ⊆ B && B ⊆ A ⟷ A === B, men i JavaScript er der nogle komplikationer med dette. I JavaScript er an Arrayet ordnet sæt og kan indeholde duplikatværdier, så vi kan ikke antage, at den tovejs delsætkode B.every(el => A.includes(el)) && A.every(el => B.includes (el)) indebærer, at a- rstrålerne Aog B er lig l. Hvis Aog B er Sæt (hvilket betyder, at de blev oprettet with newSæt ()), så er deres værdier unikke, og vi kan udføre den tovejs delsætskontrol til s ee 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)), givet det Aog Bare konstrueret using newsæt ()

Oversættelse af logik til engelsk

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

Forhåbentlig har dette alle været nyttige. Ikke kun er færdighederne i at oversætte mellem engelsk og kode nyttige, men det er praktisk at have terminologien til at diskutere forskellige forhold (som sammenhænge og implikationer) og værktøjerne til at evaluere dem (sandhedstabeller).