While this example is fairly obviously a valid argument, we can analyze it using a truth table by representing each of the premises symbolically. We can then form a conditional statement showing that the premises together imply the conclusion. If the truth table is a tautology (always true), then the argument is valid.
import itertools, re
# A tiny recursive-descent parser for ~ (not) ^ (and) v (or) -> (if..then) <-> (iff),
# weakest-binding operator first. Parenthesise nested conditionals: (p -> q) -> r.
BINARY = [("<->", lambda a, b: a == b), ("->", lambda a, b: (not a) or b),
("v", lambda a, b: a or b), ("^", lambda a, b: a and b)]
def compile_expr(text):
toks = re.findall(r"<->|->|[~^()]|\bv\b|[A-Za-z]\w*", text)
pos = 0
def parse(level=0):
nonlocal pos
if level == len(BINARY): # an atom: name, ~atom, or ( ... )
tok = toks[pos]; pos += 1
if tok == "(":
inner = parse(); pos += 1 # step over the closing bracket
return inner
if tok == "~":
inner = parse(len(BINARY))
return lambda env: not inner(env)
return lambda env, name=tok: env[name]
left = parse(level + 1)
sym, op = BINARY[level]
while pos < len(toks) and toks[pos] == sym:
pos += 1
right = parse(level + 1)
left = (lambda l, r, o: lambda env: o(l(env), r(env)))(left, right, op)
return left
return parse()
def check(title, premises, conclusion):
parts = premises + [conclusion]
names = sorted({v for p in parts for v in re.findall(r"\b(?!v\b)[A-Za-z]\w*\b", p)})
fs = [compile_expr(p) for p in parts]
mark = lambda x: "T" if x else "F"
print(f"{title}\n is [" + " ^ ".join(f"({p})" for p in premises)
+ f"] -> ({conclusion}) true on EVERY row?")
head = "".join(f"{v:>4}" for v in names) + " |" + "".join(f"{p:>12}" for p in premises) \
+ f"{'premises':>12}{conclusion:>12}{'whole':>8}"
print(" " + head)
bad = []
for combo in itertools.product([True, False], repeat=len(names)):
env = dict(zip(names, combo))
vals = [f(env) for f in fs]
allp, conc = all(vals[:-1]), vals[-1]
whole = (not allp) or conc
if not whole:
bad.append(env)
print(" " + "".join(f"{mark(env[v]):>4}" for v in names) + " |"
+ "".join(f"{mark(x):>12}" for x in vals[:-1])
+ f"{mark(allp):>12}{mark(conc):>12}{mark(whole):>8}")
if bad:
row = ", ".join(f"{k}={mark(v)}" for k, v in bad[0].items())
print(f" a row came out F, so it is not a tautology -> INVALID ({row})\n")
else:
print(" every row is T, so the statement is a tautology -> VALID\n")
check("If you bought bread you went to the store; you bought bread; so you went.",
["b -> s", "b"], "s")
check("If I have a shovel I can dig; I dug a hole; so I had a shovel.",
["S -> D", "D"], "S")
check("Mall means jeans; jeans mean a shirt; so the mall means a shirt.",
["m -> j", "j -> s"], "m -> s")
# Try it: ["p -> q", "~q"] therefore "~p" is valid; ["p -> q", "~p"] therefore "~q" is not.
We’ll let represent “you bought bread” and s represent “you went to the store”. Then the argument becomes:
To test the validity, we look at whether the combination of both premises implies the conclusion; is it true that
Since the truth table for is always true, this is a valid argument.