Implication and equivalence: if-then, vacuous truth, and guard clauses
p → q is false in exactly one case: p true, q false. A false p makes the rule hold vacuously. Converse and inverse are NOT equivalent to it — the contrapositive is, and every guard clause and early return in your code is contrapositive reasoning in disguise.
The checkout validator had a line that looked lazy: if (order.coupon) assertValid(order.coupon). A new hire read the business rule — “every order’s coupon must be valid” — and decided the code was missing a case: orders without a coupon were never checked at all. The “fix” made validation fail whenever the coupon was missing. Roughly 80% of orders carry no coupon; checkout revenue flatlined for forty minutes. The original code was correct. The business rule is an implication — IF an order has a coupon, THEN it must be valid — and an implication whose if-part is false counts as true automatically. An order with no coupon satisfies the rule by doing nothing. That is called vacuous truth, and it quietly underlies your every() calls, your guard clauses, and half the early returns you have ever written.
After this lesson you can read the truth table for p → q, explain vacuous truth and where it shows up in array methods, identify the contrapositive (and why it is the only equivalent transform), and explain why guard clauses are contrapositive reasoning in code.
Implication p → q is false in exactly one row: p true and q false. The implication p → q reads “if p, then q.” Its truth table has a shape worth memorizing: false in exactly one row.
p | q | p → q |
|---|---|---|
| T | T | T |
| T | F | F |
| F | T | T |
| F | F | T |
The best mental model is a promise: “If you pass the exam, I will buy you ice cream.” When have I broken my word? Only when you passed and got no ice cream — p true, q false. You passed and got ice cream: promise kept. You failed and got ice cream anyway: I promised nothing about that case. You failed, no ice cream: also not broken.
When p is false the implication is vacuously true. The two bottom rows say: when p is false, p → q is true no matter what q is. This is vacuous truth — the rule holds because it was never put to the test. “If an order has a coupon, it must be valid” is satisfied by a coupon-less order. Your standard library is built on it: [].every(n => n > 0) returns true because “for all elements, n > 0” has no element to fail the test. Code that does if (items.every(ok)) proceed() will proceed on an empty list — usually right, but worth a deliberate decision.
Implication compiles to !p || q in code. Since p → q is false only when p is true and q false, it is equivalent to !p || q:
// Rule: if an order has a coupon, the coupon must be valid.
const satisfiesRule = (order) => !order.coupon || isValid(order.coupon);Couponless order: !false → true — rule satisfied vacuously. Order with a coupon: !true || isValid(...) → evaluates isValid. Strengthening this to order.coupon && isValid(order.coupon) — requiring the coupon to exist — is the bug the new hire introduced; it makes every couponless order fail.
Only the contrapositive is equivalent; converse and inverse are traps. From p → q you can manufacture three related statements:
- Converse:
q → p— the arrow reversed. Not equivalent. - Inverse:
!p → !q— both sides negated. Not equivalent (same thing as the converse). - Contrapositive:
!q → !p— reversed and negated. Equivalent — same truth table.
Guard clauses cash in on this equivalence. The rule “if we process a request, it must be authenticated” (process → authed) has contrapositive “if not authenticated, do not process” — which is literally if (!req.authed) return reject(req). Each early return enforces one contrapositive, and what survives to the bottom satisfies every rule.
Rewrite the rule “if a user is premium, they see no ads” as a guard clause using the contrapositive.
The rule: premium → noAds
Contrapositive: !noAds → !premium, i.e., “if a user sees ads, they are not premium.”
As a guard clause in the rendering path:
function renderPage(user, content) {
if (user.seesAds && user.isPremium) {
throw new Error("Invariant violated: premium user must not see ads");
}
// ... rest of rendering
}Or applied at the access-control layer:
function showPremiumBadge(user) {
if (!user.isPremium) return null; // contrapositive guard
return <PremiumBadge />;
}Note what is not equivalent: inferring “if they see no ads, they must be premium” (the converse). A free-trial tier can also hide ads, so that inference fails.
▸Why this works
Why define “false implies anything” as true, rather than false or undefined? Because universal statements must survive irrelevant cases. “Every multiple of 4 is even” is clearly true. Formally: if x is a multiple of 4, then x is even, for all x. Test x = 3: the antecedent is false. If that made the implication false, x = 3 would disprove a true theorem about multiples of 4 — absurd. Defining the untested case as true is the only choice that lets rules quantify over a messy world while being judged only where they actually apply.
p → q, where p = T and q = F. Is the implication T or F?
p → q, where p = F and q = F. Is the implication T or F?
[].every(n => n > 0) in JavaScript — is this true or false?
Which is equivalent to p → q: (a) !p || q, or (b) p && q?
What is the contrapositive of 'if it rains, the ground is wet'?
Which statement is logically EQUIVALENT to 'if a user is premium, they see no ads'?
The implication p → q is a promise that is false in exactly one truth-table row: antecedent true, consequent false — condition met, promise broken. It claims no causation, only a constraint on truth values, which is why it compiles to !p || q in code. When p is false the implication is vacuously true: the rule was never tested, so it stands — an empty array passes every(), a couponless order satisfies “if there is a coupon it must be valid.” Strengthening an implication to a conjunction (requiring p AND q) is a different and far stronger proposition — the difference is precisely every case where p fails. From p → q you can form the converse q → p, the inverse !p → !q, and the contrapositive !q → !p; only the contrapositive is equivalent. Guard clauses cash in this equivalence: “if we process, then authenticated” becomes the early return “if not authenticated, reject”, and a stack of guards leaves the bottom of the function in a state where every rule provably holds. Full equivalence p ↔ q — both directions at once — is the license to substitute conditions freely.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.