Expressions in predicate logic with a single quantifier can generally
be translated into English as either “there exists an element
To begin, recall our
This allowed us to express statements like
Let’s now introduce a new way of representing the
Sophia | Thelonious | Stanley | Laura | |
---|---|---|---|---|
Breanna | False | True | True | False |
Malena | False | True | True | True |
Patrick | False | False | True | False |
Ella | False | False | True | True |
In this table format, we can interpret the two above statements as
follows: -
The two statements we just looked at used just one variable (
We translate this as “for every person
In general, when we have two consecutive universal quantifiers the
order does not matter. That is, the following two formulas are
equivalent:Tip: when the domains of the two variables are the
same, we typically combine the quantifications, e.g.,
The same is true of two consecutive existential quantifiers. Consider
the analogous formula except with
We translate this as “there exists a person
The statements “there exist an
Even though consecutive quantifiers of the same type are pretty straightforward, this is not the case when we have two different quantifiers for the variables, which we call alternating quantifiers.
For example, consider this predicate formula:
This can be translated as “For every person
Breanna | Thelonious |
Malena | Laura |
Patrick | Stanley |
Ella | Stanley |
Note that the choice of person who
Let us contrast this with the similar-looking formula, where the
order of the quantifiers has changed:
For our definition of the Loves predicate, this statement happens to
be True because everyone in
Loved by everyone in |
|
---|---|
Sophia | No |
Thelonious | No |
Stanley | Yes |
Laura | No |
But the statement would not be True if, for example, we
removed the love connection between Malena and Stanley. In this case,
Stanley would no longer be loved by everyone, and so no one in
So we would have a case where switching the order of quantifiers
changes the meaning of a formula! In both cases, the existential
quantifier
When reading a nested quantified expression, you should read it from left to right, and pay attention to the order of the quantifiers. In order to see if the statement is True, whenever you come across a universal quantifier, you must verify the statement for every single value that this variable can take on. Whenever you see an existential quantifier, you only need to exhibit one value for that variable such that the statement is True, and this value can depend on the variables to the left of it, but not on the variables to the right of it.
Now let’s see how we could represent this example in Python. First, recall the table of who loves whom from above:
Sophia | Thelonious | Stanley | Laura | |
---|---|---|---|---|
Breanna | False | True | True | False |
Malena | False | True | True | True |
Patrick | False | False | True | False |
Ella | False | False | True | True |
And we can represent this table of who loves whom in Python as a list of lists.
[False, True, True, False], # The "Breanna" row
[False, True, True, True], # The "Malena" row
[False, False, True, False], # The "Patrick" row
[False, False, True, True] # The "Ella" row
[ ]
Our list is the same as the table above, except with the people’s
names removed. Each row of the table represents a person from
set
Now, how are we going to access the data from this table? For this
section we’re going to put all of our work into a new file called
loves.py
, and so we’ll start by defining a new variable in
this file:
# In loves.py
= [
LOVES_TABLE False, True, True, False],
[False, True, True, True],
[False, False, True, False],
[False, False, True, True]
[ ]
This is the first time we’ve defined a variable within a Python file
(rather than the Python console) that is not in a function
definition. Variables defined in this way are called global
constants, to distinguish them from the local variables defined
within
functions. The term “constant” is not important right now, but
will become very important later in the course. In Python, global
constants typiecally use the ALL_CAPS
naming convention to
distinguish them from local variables.
Global constants are called “global” because their scope is the entire Python module in which they are defined: they can be accessed anywhere in the file, including all function bodies. They can also be imported and used in other Python modules, and are available when we run the file in the Python console.
LOVES_TABLE
To start, let’s run our loves.py
file in the Python
console so we can play around with the LOVES_TABLE
value.
Because LOVES_TABLE
is a list of lists, where each inner
list represents a row of the table, it’s easy to access a single row
with list indexing:
>>> LOVES_TABLE[0] # This is the first row of the table
False, True, True, False] [
From here, we can access individual elements of the table, which
represent an individual value of the
>>> LOVES_TABLE[0][1] # This is the (0, 1) entry in the table
True
>>> LOVES_TABLE[2][3] # This is the (2, 3) entry in the table
False
In general, LOVES_TABLE[i][j]
evaluates to the entry in
row i
and column j
of the table. Finally,
since the data is stored by rows, accessing columns is a little more
work. To access column j
, we can use a list comprehension
to access the j
-th element in each row. Here is an example
for accessing the leftmost column (j = 0
):
>>> [LOVES_TABLE[i][0] for i in range(0, 4)]
False, False, False, False] [
Now, let’s return to our Python file loves.py
and define
a version of our LOVES_TABLE
.
# In loves.py
= [
LOVES_TABLE False, True, True, False],
[False, True, True, True],
[False, False, True, False],
[False, False, True, True]
[
]
= {
A 'Breanna': 0,
'Malena': 1,
'Patrick': 2,
'Ella': 3
}
= {
B 'Sophia': 0,
'Thelonius': 1,
'Stanley': 2,
'Laura': 3,
}
Next, we define a loves
predicate, which takes in two
strings and returns whether person a
loves person
b
. Note that because this function is defined in the same
file as LOVES_TABLE
, it can access that global constant in
its body.
def loves(a: str, b: str) -> bool:
"""Return whether person a loves person b.
Assuming that:
- a in A
- b in B
>>> loves('Breanna', 'Sophia')
False
"""
= A[a]
a_index = B[b]
b_index return LOVES_TABLE[a_index][b_index]
Now that we’ve seen how to access individual entries, rows, and
columns from the table, let’s turn to how we would represent the
statements in predicate logic we’ve written in this section. First, we
can express
>>> all({loves(a, b) for a in A for b in B})
False
And similarly, we can express
>>> any({loves(a, b) for a in A for b in B})
True
These two examples illustrate how Python’s all
and
any
functions naturally enable us to express multiple
quantifiers of the same type. But what about the expressions we looked
at with alternating quantifiers? Consider
>>> all({any({loves(a, b) for b in B}) for a in A})
True
Though this Python expression is structurally equivalent to the
statement in predicate logic, it’s longer and a bit harder to read. In
general we try to avoid lots of expression nesting in programming, and a
rule of thumb we’ll try to follow in this course is to avoid nesting
all
/any
calls. Instead, we can pull out
the inner any
into its own function, which not only reduces
the nesting but makes it clearer what’s going on:
def loves_someone(a: str) -> bool:
"""Return whether a loves at least one person in B.
Assuming that:
- a in A
"""
return any({loves(a, b) for b in B})
>>> all({loves_someone(a) for a in A})
True
Similarly, we can express the statement any
/all
:
>>> any({all({loves(a, b) for a in A}) for b in B})
True
And by pulling out the inner all
expression into a named
function:
def loved_by_everyone(b: str) -> bool:
"""Return whether b is loved by everyone in A.
Assuming that:
- b in B
"""
return all({loves(a, b)} for a in A)
>>> any({loved_by_everyone(b) for b in B})
True
And for your reference, here is our final loves.py
file:
# In loves.py
= [
LOVES_TABLE False, True, True, False],
[False, True, True, True],
[False, False, True, False],
[False, False, True, True]
[
]
= {
A 'Breanna': 0,
'Malena': 1,
'Patrick': 2,
'Ella': 3
}
= {
B 'Sophia': 0,
'Thelonius': 1,
'Stanley': 2,
'Laura': 3,
}
def loves(a: str, b: str) -> bool:
"""Return whether a loves at least one person in B.
Assuming that:
- a in A
- b in B
>>> loves('Breanna', 'Sophia')
False
"""
= A[a]
a_index = B[b]
b_index return LOVES_TABLE[a_index][b_index]
def loves_someone(a: str) -> bool:
"""Return whether a loves at least one person in B.
Assuming that:
- a in A
"""
return any({loves(a, b) for b in B})
def loved_by_everyone(b: str) -> bool:
"""Return whether b is loved by everyone in A.
Assuming that:
- b in B
"""
return all({loves(a, b) for a in A})
In this section, we’ve introduced the notion of lists within lists to represent truth tables for binary predicates. In Chapter 5, we’ll study tabular data more generally, as well as other forms of nested collections of data. We’ll see how these more complex data structures can be used to represent real-world data for our programs.