data Blog = Blog { me :: Programmer, posts :: [Opinion] }

On Symbolic Logic

I went to public school in the United States, and I was always “bad at math.” I hated homework and tests, and it never got better: as I went through the grades, the problems got longer and more intricate, and I inevitably would make a calculation error—misplacing a decimal or flubbing an easy sum—halfway through. I got used to seeing $\color{red}{-\frac{1}{2}}$ on my papers. Those add up!

In college, the first thing I did when planning my path through general education was look for a way to avoid Calculus. My carefully selected “college statistics” course had not transferred from high school, so I needed something else. I found “Symbolic Logic,” hidden away in the philosophy department. Math? In a philosophy course? That weird math credit ended up being one of my favorite college courses, and the most relevant to my career.

What is it?

Logic has a long history. If you’ve studied philosophy or rhetoric, you’ll be familiar with propositional logic, where an argument is laid out in the form of premises and a conclusion. The author’s argument is that the conclusion she proposes follows from the premises she asserts:

  1. Socrates is a man.
  2. All men are mortal.
  3. Therefore, Socrates is mortal.

Even informal essays and opinion pieces follow very similar thesis/argument/conclusion structures.1 This structure can be expressed formally:

Socrates ($S$) is a member of set $M$, "Men"; $S \in M$
All members $m$ of set $M$ are members of set $D$, "mortals"; $\forall m \in M \Rightarrow m \in D$
Therefore, $S$ is a member of set $D$. $\therefore S \in D$

This symbolic representation, along with closely related concepts like set theory, allows us to express relationships between premises and conclusions without some of the baggage of a particular natural language. For example, take “and” and “but.” These English words are linguistically different, but are both represented in symbols by a single concept: logical conjunction ($\land$ or $\&$). This avoids the contradiction implied by “but.” Other constructs can be represented as well:

Neither $A$ nor $B$. $\lnot (A \lor B)$
Either $A$ or $B$, but not both. $A \oplus B$
Both $A$ and $B$, or neither. $\lnot (A \oplus B)$

By now, even if you haven’t learned symbolic logic before, you are probably starting to think this feels a little familiar. Haven’t we seen them somewhere before?

neither = not (a or b)
xor = a ^ b
xnor = not (a ^ b)

Comprehension

All boolean (and “bitwise”) operators are equivalent to logical operations. Understanding the latter allowed me to become a much better and more confident programmer. They also gave me one of the least sexy tools in my programming arsenal, which I originally used to learn these concepts in that “philosophy” class: truth tables.

A truth table, universally despised by beginning students of logic, is a table that you build to understand the “output” of a statement. For example, the truth table for a simple statement like $A \land B$, or “$A$ and $B$,” would look like this:

$A$ $B$ $A \land B$ $\therefore$
T T T T
TFFF
FTFF
FFFF

In each row, the truth value of each proposition (e.g. $A$) is written. To the right, the truth value of each section of the statement is derived from the inputs. Finally, the truth value of the overall statement is written at the right, although it’s not necessary for a statement this simple.

For a more complex example, consider the statement $(A \land B) \lor C$, which means “either $C$ is true or both $A$ and $B$ are true.”

$A$ $B$ $C$ $A \land B$ $\space\ldots \lor C$ $\therefore$
T T T T T T
TTFTTT
TFTFTT
TFFFFF
FTTFTT
FTFFFF
FFTFTT
FFFFFF

I once argued with a colleague for over an hour because he had written a feature with contradictory conditionals, something equivalent to $A \land \lnot A$.

def feature(a, b):
    if a:
        do_something()

        if b and not a:
            do_something_else()

No matter what I did, I couldn’t make him understand that do_something_else() could never be reached. The way that I finally got through to him was to write the code on a board and make him trace the path for each combination, (True, True), (True, False), and so on, with a marker, and then make a table that said “yes” or “no” for whether the function call was reached—a stealth truth table.2

Automate the Boring Stuff

Truth tables are laborious to construct. They have $2^n$ rows, where $n$ is the number of propositions in your statement. The columns grow as you add more operators. Happily, the congruence between boolean logic in programming languages and the more formal operators of symbolic logic means that we can put Python to work for us.

# We will use `product`, from the `itertools` package, to generate arguments to our
# functions.
from itertools import product

# This is a module that allows us to inspect and interact with functions at a higher
# level than usual, for metaprogramming. We'll use it here to find out how many
# arguments a function takes.
from inspect import signature

# This function accepts a function, `f`, that will be called with the correct number of
# arguments and have its resultant truth table generated.
def truth_table(f):
    # Inspect the function argument and find the number of parameters it needs.
    num_args = len(signature(f).parameters)

    # Generate a set of 1-8 premise labels (a, b, c...)
    premises = " ".join("abcdefgh"[:num_args])

    # Create a simple table header, labeled with premises and the function name.
    header = f"{premises} | {f.__name__}"
    print(header)
    print("-" * len(header))

    # Generate tuples of boolean values to serve as function arguments, and loop over
    # each combination, printing the result of passing them to the function.
    for args in product((True, False), repeat=num_args):
        premise_values = " ".join("T" if arg else "F" for arg in args)
        function_result = "T" if f(*args) else "F"
        print(f"{premise_values} | {function_result}")

We can define a function that looks a lot like our more complex example above, $(A \land B) \lor C$.

def a_and_b_or_c(a, b, c):
    return (a and b) or c

Calling truth_table(a_and_b_or_c) produces a beautiful ASCII rendition of a truth table. Try it out!

Conclusions

I didn’t know symbolic logic existed before I saw it listed in the course catalog. That it became a course I loved, and has helped me build a career that I enjoy, illustrates the serendipity of learning: we don’t always know what we don’t know, nor the significance of that gap. We must always strive to find and fill them.

Exercises

Since this is a “knowledge” post rather than an opinion piece I thought I’d add an appendix here with some exercises.

  1. Use the built-in operator module in Python and generate truth tables for all of the basic logical operations.3
  2. $\star$ The truth_table function can generate an overall truth table for a given function of [bool...] -> bool, but it does not give separate columns for all of the “substatements,” as illustrated in the manually-built truth table above. Implement an alternative that can. (It doesn’t need to accept arbitrary functions, though.)
  3. The concept of “validity” enters the picture emerges if there is a statement like $A \Rightarrow B$ ("$A$ implies $B$"), but $A$ is true and $B$ is false. Using your improved generator from exercise two, report when a given row of a truth table is invalid.

Enjoy!


  1. There, the challenge is usually judging whether the premises are true, rather than whether the argument is sound. ↩︎

  2. I found it hard to have empathy. It was a heated discussion. Over the years, though, I’ve learned that all of us have gaps. I hope I helped him fill one of his. I wish I could’ve been more patient at that time. ↩︎

  3. The documentation is comprehensive. Note that some functions, like and_ and or_, are suffixed with an underscore to avoid collision with Python syntax. ↩︎