Végleges útmutató a JavaScript feltételes logikájához

Front-end mérnök és matematikus vagyok. Naponta támaszkodom matematikai képzésemre kódírás közben. Nem statisztikákat vagy számításokat használok, hanem a logikai logika alapos megértését. Gyakran váltottam az ampersand, a pipa, a felkiáltójel és az egyenlő jelek összetett kombinációját egyszerűbbé és sokkal olvashatóbbá. Szeretném megosztani ezeket az ismereteket, ezért megírtam ezt a cikket. Hosszú, de remélem, ugyanolyan előnyös lesz számodra, mint nekem. Élvezd!

Igazság és hamis értékek a JavaScript-ben

A logikai kifejezések tanulmányozása előtt értsük meg, mi az „igazság” a JavaScript-ben. Mivel a JavaScript lazán be van írva, logikai kifejezésekben kényszeríti az értékeket logikai értékekre. ifkimutatások &&, ||és háromkomponensű feltételek minden kényszerítsék értékek logikai értékek. Ne feledje, hogy ez nem azt jelenti, hogy a műveletből mindig logikai értéket adnak vissza.

Már csak hat falsy értékeket JavaScript - false, null, undefined, NaN, 0, és ""- és minden más truthy . Ez azt jelenti, []és {}mindkettő igaz, amelyek általában felborítják az embereket.

A logikai operátorok

A formális logikában csak néhány operátor létezik: tagadás, együttállás, diszjunkció, implikáció és kétfeltétel. Mindegyik rendelkezik egy JavaScript egyenértékű: !, &&, ||, if (/* condition */) { /* then consequence */}, és ===, ill. Ezek az operátorok létrehozzák az összes többi logikai utasítást.

Igazságtáblák

Először nézzük meg az egyes alapvető operátorok igazságtáblázatait . Az igazságtábla megmondja, hogy egy kifejezés valóságtartalma mi alapján épül fel részei valóságtartalmán . Az igazságtáblázatok fontosak. Ha két kifejezés ugyanazt az igazságtáblát generálja, akkor ezek a kifejezések ekvivalensek és helyettesíthetik egymást .

A Negation táblázat nagyon egyszerű. A negáció az egyetlen unáris logikai operátor, amely csak egyetlen bemenetre hat. Ez azt jelenti, hogy !A || Bez nem ugyanaz, mint !(A || B). A zárójelek úgy viselkednek, mint a matematikában megtalált csoportosítás.

Például a Negáció igazságtáblázat első sorát (lent) a következőképpen kell olvasni: „Ha az A állítás igaz, akkor az A kifejezés hamis.”

Egy egyszerű állítás megtagadása nem nehéz. Az „eső esik” tagadás „ nem esik”, és a JavaScript primitívjének tagadása truetermészetesen az false. Az összetett állítások vagy kifejezések tagadása azonban nem ilyen egyszerű. Mi a „ mindig esik az eső” vagy isFoo && isBar?

Az együttállás táblázat azt mutatja, hogy a kifejezés A && Bcsak akkor igaz, ha mindkét A és B igaz. Ennek ismernie kell a JavaScript írását.

A Disjunction táblának szintén nagyon ismerősnek kell lennie. A diszjunkció (logikai VAGY utasítás) igaz, ha egyik vagy mindkettőA és B értéke igaz.

Az Implication táblázat nem annyira ismerős. Mivel A feltételezi B, A, hogy igaz azt jelenti, B igaz. A B azonban A-tól eltérő okokból is igaz lehet, ezért igaz a táblázat utolsó két sora. Csak akkor hamis a következtetés, amikor A igaz és B hamis, mert akkor A nem utal B-re.

Míg az ifutasításokat implikációkra használják a JavaScript-ben, nem minden ifutasítás működik így. Általában ifáramlásszabályozásként használjuk, nem pedig valósághűség-ellenőrzésként, ahol a következmény is számít az ellenőrzés során. Íme az archetipikus implikációif :

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

Ne aggódjon, hogy ez kissé kínos. Az implikációk kódolásának egyszerűbb módjai vannak. Ezen kínosság miatt azonban továbbra is a jelen cikkben a következmények szimbólumaként fogom használni .

A Bicondition operátor, amelyet néha if-and-only-if (IFF) -nek is hívnak, csak akkor értékeli igaznak, ha a két operandus, A és B azonos igazságértékkel bír. Mivel a JavaScript kezeli az összehasonlításokat, a ===logikai célokra való felhasználást csak logikai elemekre leadott operandusokra szabad használni. Vagyis ahelyett A === B, hogy használnunk kellene !!A === !!B.

Figyelmeztetések

Két nagy figyelmeztetés van a JavaScript kód kezelésére, mint a propozíciós logika: a rövidzárlat és a műveletek sorrendje .

A rövidzárlat olyasmi, amit a JavaScript-motorok időt takarítanak meg. Nem értékeljük azt, ami nem változtatja meg a teljes kifejezés kimenetét. A doSomething()következő példákban szereplő függvényt soha nem hívják meg, mert bármit is adott vissza, a logikai kifejezés eredménye nem változik:

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

Emlékezzünk arra, hogy az ( &&) kötőszók csak akkor igazak , ha mindkét állítás igaz , a disjunkciók ( ||) pedig csak akkor hamisak , ha mindkét állítás hamis. Ezen esetek mindegyikében az első érték elolvasása után nincs szükség további számításokra a kifejezések logikai kimenetelének értékeléséhez.

Ezen funkció miatt a JavaScript néha megszakítja a logikai kommutativitást. Logikailag A && Begyenértékű B && A, de megtöri a programot, ha felcserélt window && window.mightNotExistbe window.mightNotExist && window. Ez nem azt jelenti, hogy az ingázott kifejezés valódisága különbözik egymástól, csupán az, hogy a JavaScript hibát okozhat annak elemzésében.

A műveletek sorrendje a JavaScript elkapott a meglepetés, mert nem tanították, hogy a formális logika volt egy műveletek sorrendje eltérő csoportosításával és balról-jobbra. Kiderült, hogy sok programozási nyelv &&magasabb prioritással bír, mint a ||. Ez azt jelenti, hogy &&először csoportosítva (nem értékelve) balról jobbra, majd ||balról jobbra csoportosítva. Ez azt jelenti, hogy A || B && Cmostantól nem értékelték ugyanúgy (A || B) && C, mint inkább A || (B && C).

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

Szerencsére, csoportosítás , ()tart a legfelső megelőzi a JavaScript. A meglepetéseket és a kétértelműségeket elkerülhetjük, ha az értékelni kívánt állításokat kézzel diszkrét kifejezésekké kapcsoljuk össze. Éppen ezért sok kódinterjú megtiltja mindkettőnek &&és ||ugyanazon csoportnak a meglétét .

Összetett igazságtáblák kiszámítása

Most, hogy az egyszerű állítások valóságtartalma ismert, kiszámítható az összetettebb kifejezések igazságtartalma.

Először számolja meg a változó számát a kifejezésben, és írjon egy igazságtáblát, amely 2 that sorral rendelkezik.

Ezután hozzon létre egy oszlopot az egyes változókhoz, és töltse ki őket az igaz / hamis értékek minden lehetséges kombinációjával. Azt javaslom, hogy töltse ki az első oszlop első felét Ta második felével F, majd a következő oszlopot negyedelje fel, és így tovább, amíg ez így nem néz ki:

Ezután írja le a kifejezést és oldja meg rétegekben, a legbelső csoportoktól kifelé, az igazságértékek minden kombinációjára:

Mint fentebb említettük, az ugyanazt az igazságtáblát előállító kifejezések helyettesíthetők egymással.

A cserék szabályai

Most néhány példát ismertetek a cserék szabályairól, amelyeket gyakran használok. Az alábbiakban nem szerepelnek igazságtáblázatok, de saját maga is elkészítheti azokat annak igazolására, hogy ezek a szabályok helyesek.

Kettős tagadás

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 : Bha-akkor-más definíció

A ? !B : B kizárólagos vagy (xor) a JavaScript-ben

Alternatív megoldásként

1. „A vagy B, de nem egyaránt A és B”

2. (A || B) && !(A && B)közvetlen fordítás

3. (A || B) && (!A || !B)DeMorgan törvényei

4. (A && !A) || (A && !B) || (B && !A) || (B && !B)kettős elosztás

5. (A && !B) || (B && !A)ellentmondáspótlás

6. A === !Bvagy A !== Banyagi egyenértékűség

A === !B vagyA !== B xor a JavaScript-ben

Logika beállítása

Eddig két (vagy néhány) értéket magában foglaló kifejezésekről szóló állításokat kerestük, de most az értékkészletekre fogjuk fordítani a figyelmünket. Hasonlóan ahhoz, ahogyan az összetett kifejezések logikai operátorai kiszámítható módon őrzik az igazságot, a halmazok predikátumfüggvényei kiszámítható módon őrzik az igazságot.

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.

Példaként vegyük figyelembe a tömböt [2, 4, 6, 8]és az egyetemes állítást: „A tömb minden eleme egyenletes.” A isEvenJavaScript beépített univerzális funkciójának használatával futtathatjuk [2, 4, 6, 8].every(isEven)és megállapíthatjuk, hogy ez az true.

Array.prototype.every a JavaScript univerzális nyilatkozata

Létező nyilatkozatok

Egy egzisztenciális utasítás konkrét állítást tesz egy halmazról: a halmaz legalább egy eleme igazat ad vissza a predikátumfüggvényre. Ha az állítmány hamis értéket ad vissza a halmaz minden elemére, akkor az egzisztenciális utasítás hamis.

JavaScript is szállít egy beépített egzisztenciális nyilatkozata: Array.prototype.some. Hasonló every, somekorán visszatér (igaz), ha egy elem kielégíti az állítmányát. Például [1, 3, 5].some(isOdd)csak az állítmány isOdd(iterelés 1és visszatérés true) és a visszatérés egyetlen iterációját futtatja true. [1, 3, 5].some(isEven)visszatér false.

Array.prototype.some a JavaScript egzisztenciális nyilatkozata

Univerzális implikáció

Ha például egy univerzális állítást összehasonlítottunk egy halmazsal, mondjuk nums.every(isOdd), csábító arra gondolni, hogy megragadhat egy elemet a halmazból, amely kielégíti az állítmányt. Van azonban egy fogás: a logikai logikában egy igazi univerzális állítás nem jelenti azt, hogy a halmaz nem üres. Az üres halmazokról szóló egyetemes állítások mindig igazak , ezért ha egy halmazból meg akar ragadni egy elemet, amely teljesít valamilyen feltételt, használjon egzisztenciális ellenőrzést. Ennek bizonyítására futtassa [].every(() => false). Igaz lesz.

Az üres halmazokról szóló egyetemes állítások mindig igazak .

Univerzális és egzisztenciális kijelentések megtagadása

Ezen állítások tagadása meglepő lehet. Az egyetemes állítás tagadása mondjuk nums.every(isOdd)nem az nums.every(isEven), hanem inkább nums.some(isEven). Ez egy egzisztenciális állítás, az állítmány tagadva. Hasonlóképpen, az egzisztenciális állítás tagadása egyetemes állítás, az állítmány tagadásával.

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

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

Állítsa be a kereszteződéseket

Két halmaz csak néhány szempontból kapcsolható össze egymással, elemeik tekintetében. Ezek a kapcsolatok könnyen ábrázolhatók a Venn-diagramokkal, és (többnyire) kódban meghatározhatók univerzális és egzisztenciális állítások kombinációjával.

Két halmaz megoszthatja egyes elemeit, de nem minden elemét, mint egy tipikusan összekapcsolt Venn-diagram:

A.some(el => B.includes(el)) && A.some(el => !B.includes(el)) && B.some(el => !A.inclaz ude (el)) összeillesztett halmazpárt ír le

Az egyik halmaz tartalmazhatja a többi halmaz összes elemét, de vannak olyan elemei, amelyeket a második halmaz nem oszt meg. Ez egy részhalmaz- kapcsolat, amelyet jelölünk Subset ⊆ Superset.

B.every(el => A.includes(el)) leírja a B ⊆ A részhalmaz viszonyt

A két halmaz nem oszthat meg elemeket. Ezek diszjunkt halmazok.

A.every(el => !B.includes(el)) egy szétválasztott halmazpárt ír le

Végül a két halmaz minden elemet megoszthat. Vagyis egymás részhalmazai. Ezek a halmazok egyenlőek . A formális logikában írnánk A ⊆ B && B ⊆ A ⟷ A === B, de a JavaScript-ben ennek vannak bizonyos bonyodalmai. A JavaScript-ben az Arrayan rendezett halmaz, és duplikált értékeket tartalmazhat, ezért nem feltételezhetjük, hogy az B.every(el => A.includes(el)) && A.every(el => B.includes (el) kétirányú részhalmaz kódja az a rsugarakat Aés B egyenlő l. Ha Aés B halmazok (vagyis azt hoztuk létre, hogy with newSet ()), akkor az értékeik egyediek, és a kétirányú részhalmaz ellenőrzését elvégezhetjük s ee if A=== B értékkel .

(A === B) === (Array.from(A).every(el => Array.from(B).includes(el)) && Array.from(B).every(el => Array.from(A).includes (el)), tekintettel arra , hogy a ACsupasz felépített using newkészlet ()

A Logic angolra fordítása

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.