Mathematical Logic through Python Yannai A. Gonczarowski instant download
Mathematical Logic through Python Yannai A. Gonczarowski instant download
https://ebookgate.com/product/mathematical-logic-through-python-
yannai-a-gonczarowski/
https://ebookgate.com/product/mathematical-logic-1st-edition-george-
tourlakis/
ebookgate.com
https://ebookgate.com/product/a-course-on-mathematical-logic-2nd-
edition-shashi-mohan-srivastava/
ebookgate.com
https://ebookgate.com/product/introduction-to-mathematical-logic-
fifth-edition-mendelson/
ebookgate.com
https://ebookgate.com/product/introduction-to-mathematical-logic-
sixth-edition-elliott-mendelson/
ebookgate.com
Classical Mathematical Logic The Semantic Foundations of
Logic 1, with corrections Edition Richard L. Epstein
https://ebookgate.com/product/classical-mathematical-logic-the-
semantic-foundations-of-logic-1-with-corrections-edition-richard-l-
epstein/
ebookgate.com
https://ebookgate.com/product/python-tricks-a-buffet-of-awesome-
python-features-dan-bader/
ebookgate.com
https://ebookgate.com/product/an-episodic-history-of-mathematics-
mathematical-culture-through-problem-solving-steven-g-krantz/
ebookgate.com
https://ebookgate.com/product/deep-beauty-understanding-the-quantum-
world-through-mathematical-innovation-1st-edition-hans-halvorson/
ebookgate.com
Using a unique pedagogical approach, this text introduces mathematical logic by guiding
students in implementing the underlying logical concepts and mathematical proofs via
Python programming. This approach, tailored to the unique intuitions and strengths of
the ever-growing population of programming-savvy students, brings mathematical logic
into the comfort zone of these students and provides clarity that can only be achieved
by a deep hands-on understanding and the satisfaction of having created working code.
While the approach is unique, the text follows the same set of topics typically covered in a
one-semester undergraduate course, including propositional logic and first-order predicate
logic, culminating in a proof of Gödel’s completeness theorem. A sneak peek to Gödel’s
incompleteness theorem is also provided. The textbook is accompanied by an extensive
collection of programming tasks, code skeletons, and unit tests. Familiarity with proofs
and basic proficiency in Python is assumed.
Noam Nisan is Professor of Computer Science and Engineering at The Hebrew University
of Jerusalem, serving as Dean of the School of Computer Science and Engineering during
2018–2021. He received his PhD in Computer Science from the University of California,
Berkeley. Among the awards for his research on computational complexity and algorithmic
game theory are the Gödel Prize and Knuth Award. This is his fifth book.
NOAM NISAN
Hebrew University of Jerusalem
www.cambridge.org
Information on this title: www.cambridge.org/9781108845076
DOI: 10.1017/9781108954464
© Yannai A. Gonczarowski and Noam Nisan 2022
This publication is in copyright. Subject to statutory exception
and to the provisions of relevant collective licensing agreements,
no reproduction of any part may take place without the written
permission of Cambridge University Press.
First published 2022
Printed in the United Kingdom by TJ Books Limited, Padstow Cornwall
A catalogue record for this publication is available from the British Library.
Library of Congress Cataloging-in-Publication Data
Names: Gonczarowski, Yannai A., 1981- author. | Nisan, Noam, author.
Title: Mathematical logic through Python / Yannai A. Gonczarowski, Harvard
University, Massachusetts, Noam Nisan, Hebrew University of Jerusalem.
Description: Cambridge, United Kingdom ; New York, NY : Cambridge
University Press, [2022] | Includes index.
Identifiers: LCCN 2021057959 (print) | LCCN 2021057960 (ebook) |
ISBN 9781108845076 (hardback) | ISBN 9781108949477 (paperback) |
ISBN 9781108954464 (epub)
Subjects: LCSH: Logic, Symbolic and mathematical. | Python (Computer
program language) | BISAC: COMPUTERS / Languages / General
Classification: LCC QA9 .G64 2022 (print) | LCC QA9 (ebook) |
DDC 005.13/1–dc23/eng/20220125
LC record available at https://lccn.loc.gov/2021057959
LC ebook record available at https://lccn.loc.gov/2021057960
ISBN 978-1-108-84507-6 Hardback
ISBN 978-1-108-94947-7 Paperback
Cambridge University Press has no responsibility for the persistence or accuracy
of URLs for external or third-party internet websites referred to in this publication
and does not guarantee that any content on such websites is, or will remain,
accurate or appropriate.
Preface page xi
3 Logical Operators 41
3.1 More Operators 41
3.2 Substitutions 43
3.3 Complete Sets of Operators 46
3.4 Proving Incompleteness 49
4 Proof by Deduction 53
4.1 Inference Rules 53
4.2 Specializations of an Inference Rule 56
4.3 Deductive Proofs 59
vii
Cheatsheet: Axioms and Axiomatic Inference Rules Used in This Book 266
Index 268
Mathematical Logic 101 is a beautiful course. Gödel’s Theorems are arguably the most
profound and deep truths taught throughout the entire undergrad theoretical curriculum.
Nonetheless, it seems that among many computer science and engineering students this
course suffers from the reputation of being an unintelligible course full of technical, unin-
sightful proofs. Students lose themselves in endless inductions and do not fully understand
what it means, e.g., to “prove that anything that is true can be proven.” Indeed, how can this
not be confusing when the two occurrences of “prove” in that sentence have two distinct
meanings – the latter referring to a precise very strict mathematical “proof” object that is
defined during this course, while the former refers to the free-text proofs that we have been
taught since our first year of undergrad? This book drastically reenvisions the Mathematical
Logic 101 course, conveying the same material but tapping into the strengths of the ever-
growing cohort of programming-oriented students to do so.
How does one help programming-oriented students to not lose themselves among end-
less little details in proofs, failing to see the overarching message of the course? We
set out to make this course less abstract, more intuitive, and maybe even exciting, by
tapping into the context where such students are used to comfortably dealing with endless
little details on the way to a larger goal without ever missing the forest for the trees:
computer programming. We redesigned the entirety of this very theoretical course from
scratch to be based upon a series of programming exercises, each corresponding to either a
theorem/lemma/corollary or a step toward such.
For example, the main result of the first half of a standard Mathematical Logic 101 course
is the “Tautology Theorem” (a variant of the Completeness Theorem for propositional
logic), which asserts that every tautology – every statement that holds in every possible
model or setting – can be proven to hold using a small set of axioms. The corresponding
programming exercise in this book is to write a function (based on functions from previous
exercises, of course) whose input is a formula (an object of class Formula, which the
students implement in a previous exercise) and whose output is either a model in which
this formula does not hold (i.e., a counterexample to the formula being a tautology) or a
proof (an object of class Proof, which the students implement in a previous exercise) of
this formula. Obviously, whoever can write such a function, including all its recursively
implemented helper functions, completely understands the reasoning in the proof of the
Tautology Theorem, including all its inductively proven lemmas. (And this holds even
more so for students who naturally grasp recursive code far better than they do inductive
proofs.) In our experience, students with a background in programming for the most part
even understand this proof better having actively coded its functionality themselves than
xi
had they only passively seen the proof being written on the blackboard by a teacher. In
the process of moving from proving to programming, we have in fact also disambiguated
the two meanings of “prove” in the earlier statement of “prove that whatever is true can
be proven”: we transformed the former “prove” into “program in code” and the latter “can
be proven” into “is the conclusion of a valid Proof object.” This disambiguation by way
of defamiliarization of each of these occurrences of “prove” achieves great clarity and
furthermore allows the students to more easily reexamine their intuitions and unconscious
assumptions about proofs.
This book evolved from a course that we have been teaching at the Hebrew University of
Jerusalem since 2017, first as an elective (we limited our class to 50 and then to 100 students
as we fine-tuned the course, and there had been a waiting list) and later as an alternative
for computer science and engineering students to the mandatory Mathematical Logic 101,
to the clear satisfaction of the students, who continuously rank this course highly. In our
experience, having the tasks of a single chapter due each week (if the schedule permits,
then we try to allow an additional week for Chapter 10), with the tasks of Part I (Chapters 1
through 6) being solved by each student individually and the tasks of Part II (Chapters 7
through 12) being solved in pairs, has consistently proven to work well.
We are grateful to the Hebrew University students who took our course for their valuable
questions and comments, and to our earlier students also for the faith they have put in us.
We are indebted to our first TA and beta-solver, Alon Ziv, as well as to our subsequent TAs
Noam Wies, Asaf Yehudai, Ofer Ravid, and Elazar Cohen, and beta-solvers Omri Cohen
and Matan Harsat. A special thanks goes to Chagit Schiff-Blass, at the time a Law and
Cognitive Science student, who showed us that our way of teaching Mathematical Logic
really does appeal to an even more diverse student population than we had imagined, by first
being an excellent beta-solver and then joining our teaching team. We thank Henry Cohn
for valuable advice, and thank Aviv Keren and Shimon Schocken for their valuable detailed
feedback on portions of earlier drafts of this book. We especially thank David Kashtan
for careful and valuable scientific editing of this book on the logic side; any deviations
from standard definitions or nomenclature are, of course, our own responsibility. Finally,
we thank Kaitlin Leach, Rebecca Grainger, and the entire team at Cambridge University
Press for their support and for their flexibility throughout the COVID pandemic. The cover
picture by Vasily Kandinsky is titled “Serious-Fun,” and we hope that this will describe
your experience as you work through this book. We always appreciate feedback from
readers.
Assume that all Greeks are men. Assume also that all men are mortal. It follows logically
that all Greeks are mortal.
This deduction is remarkable in the sense that we can make it even without understanding
anything about Greeks, men, or mortality. The same deduction can take the assumptions
that all Greeks are fish and that all fish fly and conclude that all Greeks fly. As long as the
assumptions are correct, so is the conclusion. If one or more of the assumptions is incor-
rect, then all bets are off and the conclusion need not hold. How are such “content-free”
deductions made? When is such a deduction valid? For example, assume that some Greeks
are men and that some men are mortal; does it follow that some Greeks are mortal? No!
The field of logic deals exactly with these types of deductions – those that do not require
any specific knowledge of the real world, but rather take statements about the world and
deduce new statements from them, new statements that must be true if the original ones are.
Such deductions are a principal way by which we can extend our knowledge beyond any
facts that we directly observe. While in many fields of human endeavor logical deductions
go hand in hand with other techniques of observing and understanding the actual facts of
the world, in the field of mathematics logical deductions serve as the sole paradigmatic
foundation.
A crucial property of logical deduction is that it is purely syntactic rather than semantic.
That is, the validity of a logical deduction can be completely determined by its form, its
syntax. Nothing about the actual meaning of the assumptions or conclusion, such as their
truth or falsehood, is involved. The usefulness, however, of such deductions comes from
the, perhaps surprising, fact that their conclusions do turn out to be true in the meaningful,
semantic, sense. That is, whenever the assumptions are true, the conclusion also happens to
be true – and this happens despite the fact that the deduction process itself was completely
oblivious to said truth! Indeed, the clear separation between syntactic notions and semantic
ones, as well as establishing the connections between them, are the core of the study of
logic. There are several different possible motivations for such study, and these different
motivations influence the type of issues emphasized.
Philosophers usually use logic as a tool of the trade, and mostly focus on the difficult
process of translating between natural human language and logical formulas.1 These are
tricky questions mostly due to the human part of this mismatch: Human language is not
completely precise, and to really understand the meaning of a sentence may require not only
1 Another frequently used plural form of “formula,” which you may encounter in many books, is “formulae.”
For simplicity, in this book we will stick with “formulas.”
logical analysis but also linguistic analysis and even social understanding. For example,
who exactly is included in the set of Greeks? When we assumed that they are all men,
does that include or exclude women? Without coming to grips with these thorny questions,
one cannot assess whether the assumptions are true and cannot benefit from the logical
deduction that all Greeks are mortal.
Mathematicians also study logic as a tool of the trade. Mathematicians usually apply
logic to precise mathematical statements, so they put less emphasis on the mismatch with
the imprecise human language, but are rather focused on the exact rules of logic and on
exactly understanding the formalization process and power of logic itself. Indeed, to under-
stand the power of logic is to understand the limits of the basic paradigm of mathematics
and mathematical proofs, and thus the field of mathematical logic is sometimes called
meta-mathematics, mathematically studying mathematics itself.
Computer scientists use logic as a tool of the trade in a somewhat different sense,
often relying on logical formalisms to represent various computational abstractions. Thus,
for example, a language to access databases (e.g., SQL) may be based on some logical
formalism (e.g., predicate logic), and abstract computational search problems (e.g., NP
problems) may be treated as finding assignments to logical formulas (e.g., SAT).
The approach of this book is to proceed toward the goal of mathematicians who study
logic, using the tools of computer scientists, and in fact not those computer scientists
who study logic, but rather more applied computer scientists. Specifically, our main goal
is to precisely formalize and understand the notions of a logical formula and a deduc-
tive logic proof, and to establish their relationship with mathematical truth. Our tech-
nique is to actually implement all these logical formulas and logical proofs as bona fide
objects in a software implementation: You will actually be asked to implement, in the
Python programming language, methods and functions that deal with Python objects such
as Formula and Proof. For example, in Chapter 2 you will be asked to implement a func-
tion is_tautology(formula) that determines if the given logical formula is a tautology,
i.e., logically always true; while in Chapter 6 you will be asked to implement a function
proof_or_counterexample(formula) that returns either a formal logical proof of the
given formula – if it happens to be a tautology – or else a counterexample that demonstrates
that this formula is in fact not a tautology.
This book has a very clear end point to which everything leads: Gödel’s completeness
theorem, named after its discoverer, the Austrian (and later American) logician and math-
ematician Kurt Gödel. To understand it, let us first look at the two main syntactic objects
that we will study and their semantics. Our first focus of attention is the formula, a formal
representation of certain logical relations between basic simpler notions. For example a
formalization of “All men are mortal” in the form, say, ‘∀x[Man(x)→Mortal(x)]’ (we will,
of course, specify exact syntactic rules for such formulas). Now comes the semantics, that
is, the notion of truth of such a formula. A formula may be true or false in a particular
setting, depending on the specifics of the setting. Specifically, a formula can be evaluated
only relative to a particular model, where this model must specify all the particulars of the
setting. In our example, such particulars would include which x in the “universe” are men
and which are mortal. Once such a model is given, it is determined whether a given formula
is true in this model or not.
Our second focus of attention is the notion of a proof. A proof again is a syntactic
object: It consists of a set of formulas called assumptions, an additional formula called
conclusion, and the core of the proof is a list of formulas that has to conform to certain
specific rules ensuring that each formula in the list “follows” in some precise syntactic
sense from previous ones or from assumptions, and that the last formula in the list is the
conclusion. If such a formal proof exists, then we say that the conclusion is (syntactically)
provable from the assumptions, which we denote by assumptions ` conclusion. Now,
again, enter the semantics, which deal with the following question: Is it the case that
in every model in which all the assumptions are true, the conclusion is also true? (This
question is only about the assumptions and the conclusion, and is agnostic of the core of
any proof.) If that happens to be the case, then we say that the conclusion (semantically)
follows from the assumptions, which we denote by assumptions |H conclusion. Gödel’s
completeness theorem states the following under certain conditions.
theorem (Gödel’s Completeness Theorem) For any set of assumptions and any
conclusion, it holds that “assumptions ` conclusion” if and only if “assumptions |H
conclusion”.
This is a very remarkable theorem connecting two seemingly unrelated notions: The
existence of a certain long list of formulas built according to some syntactic rules (this
long list is the syntactic proof just defined), and the mathematical truth that whenever all
assumptions are true, so invariably is the conclusion. On second thought, it does make
sense that if something is syntactically provable then it is also semantically true: We will
deliberately choose the syntactic rules of a proof to only allow true deductions. In fact,
this is the whole point of mathematics: In order to know that whenever we add two even
numbers we get an even number, we do not need to check all possible (infinitely many!)
pairs of even numbers, but rather it suffices to “prove” the rule that if the two numbers that
we add up are even then the result is even as well, and the whole point is that our proof
system is sound: A “proved” statement must be true (otherwise the concept of a proof
would not have been of any use). The other direction, the fact that any mathematical truth
can be proven, is much more surprising: We could have expected that the more possibilities
we build into our proof system, the more mathematical truths it can prove. It is far from
clear, though, that any specific, finite, syntactic set of rules for forming proofs should suffice
for proving, given any set of assumptions, every conclusion that follows from it. And yet,
for the simple syntactic set of logical rules that we will present, this is exactly what Gödel’s
completeness theorem establishes.
One can view this as the final triumph of mathematical reasoning: Our logical notion of
proof completely suffices to establish any consequence of any set of assumptions. Given a
set of axioms of, e.g., a mathematical field (or any other mathematical structure), anything
that holds for all fields can actually be logically proven from the field axioms!
Unfortunately, shortly after proving this completeness theorem, Gödel turned his atten-
tion to the question of finding the “correct” set of axioms to capture the properties of the
natural numbers. What was desired at the time was to find for every branch of mathematics
a simple set of axioms that suffices for proving or disproving any possible mathematical
statement in that branch.2 We say “unfortunately” since Gödel showed this to fail in a most
spectacular way, showing that no such set of axioms exists even for the natural numbers: for
every set of axioms there will remain mathematical statements about the natural numbers
that can neither be proved nor disproved! This is called Gödel’s incompleteness theorem.
Despite its name, this theorem does not in fact contradict the completeness theorem: It
is still true that anything that (semantically) follows from a set of axioms is syntactically
provable from it, but unfortunately there will always remain statements such that neither
they nor their negation follow from the set of axioms.
One can view Gödel’s incompleteness theorem as the final defeat of mathematical rea-
soning: There will always remain questions beyond the reach of any specific formalization
of mathematics. But this book – a first course in mathematical logic – focuses only on
the triumph, i.e., on Gödel’s completeness theorem, leaving the defeat, the incompleteness
theorem, for a second course in mathematical logic.
The mathematical content covered by this book is quite standard for a first course in math-
ematical logic. Our pedagogical approach is, however, unique: We will “prove” everything
by writing computer programs.
Let us motivate this unusual choice. We find that among academic courses in mathe-
matics, the introductory mathematical logic course stands out as having an unusual gap
between student perceptions and our own evaluation of its content: While we (and, we
think, most mathematicians) view the mathematical content as rather easy, students seem
to view it as very confusing relative to other mathematics courses. While we view the
conceptual message of the course as unusually beautiful, students often fail to see this
beauty – even those that easily see the beauty of, say, calculus or algebra. We believe that
the reason for this mismatch is the very large gap that exists between the very abstract point
of view – proving things about proofs – and the very low-level technical proofs themselves.
It is easy to get confused between the proofs that we are writing and the proofs that are our
subjects of discussion. Indeed, when we say that we are “writing proofs to prove things
about proofs,” the first “proofs” and the second “proofs” actually mean two very different
things even though many introductory mathematical logic courses use the same word for
both. This turns out to become even more confusing as the “mechanics” of both the proof
we are writing and the proof that we are discussing are somewhat cumbersome while the
actual point that we are making by writing these proofs is something that we usually take
for granted, so it is almost impossible to see the forest for the trees.
Computer scientists are used to combining many “mechanical details” to get a high-level
abstract goal (this is known as “programming”), and are also used to writing programs that
handle objects that are as complex as the programs themselves (such as compilers). A
large part of computer science exactly concerns the discussion of how to handle such chal-
lenges both in terms of tools (debuggers, assemblers, compilers) and it terms of paradigms
2 This desire, formulated by the German mathematician David Hilbert, was called “Hilbert’s Program.”
(interfaces, object-orientation, testing). So this book utilizes the tools of a computer scien-
tist to achieve the pedagogical goal of teaching the mathematical basis of logic.
We have been able to capture maybe 95% of the mathematical content of a standard first
course in mathematical logic as programming tasks. These tasks capture the notions and
procedures that are studied, and the solution to each of these programming tasks can be
viewed as capturing the proof for some lemma or theorem. The reader who has actually
implemented the associated function has in effect proved the lemma or theorem, a proof
that has been verified for correctness (to some extent) once it has passed the extensive
array of tests that we provide for the task. The pedagogical gain is that confusing notions
and proofs become crystal clear once you have implemented them yourself. Indeed, in the
earlier sentence “writing proofs to prove things about proofs,” the first “proofs” becomes
“code” and the second “proofs” becomes “Python objects of class Proof.” Almost all the
lemmas and theorems covered by a typical introductory course in mathematical logic are
captured this way in this book. Essentially the only exceptions are theorems that consider
“infinite objects” (e.g., an infinite set of formulas), which cannot be directly captured by a
program that is constrained to dealing with finite objects. It turns out, however, that most
of the mathematical content of even these infinitary proofs can be naturally captured by
lemmas dealing with finite objects. What remains to be made in a purely non-programmatic
mathematical way is just the core of the infinite argument, which is the remaining 5% or
so that we indeed then lay out in the classical mathematical way.
This book is centered around a sequence of programming projects in the Python program-
ming language.3 We provide a file directory that contains a small amount of code that we
have already implemented, together with many skeletons of functions and methods that you
will be asked to complete, and an extensive array of tests that will verify that your imple-
mentation is correct. Each chapter of this book is organized around a sequence of tasks,
each of which calls for completing the implementation of a certain function or method for
which we have supplied the skeleton (which also appears as a code snippet in the book).
All of our code-base, including the already implemented parts of the code, the skeletons,
and the tests, can be downloaded from the book website at www.LogicThruPython.org.
Let us take as an example Task 2 in Chapter 1. Chapter 1 deals with propositional
formulas. You will handle such objects using code that appears in the Python file
propositions/syntax.py, which already contains the constructor for a Python class
Formula for holding a propositional formula as a tree-like data structure.4
3 Specifically, the code snippets in this book have been tested with Python 3.7. Please refer to the book website
at www.LogicThruPython.org for updated information regarding compatibility of newer Python versions
with our code-base.
4 The annotations following various colon signs, as well as following the -> symbol, are called Python type
annotations and specify the types of the variables/parameters that they follow, and respectively of the return
values of the functions that they follow.
propositions/syntax.py
class Formula:
"""An immutable propositional formula in tree representation, composed from
variable names, and operators applied to them.
Attributes:
root: the constant, variable name, or operator at the root of the
formula tree.
first: the first operand of the root, if the root is a unary or binary
operator.
second: the second operand of the root, if the root is a binary
operator.
"""
root: str
first: Optional[Formula]
second: Optional[Formula]
Parameters:
root: the root for the formula tree.
first: the first operand for the root, if the root is a unary or
binary operator.
second: the second operand for the root, if the root is a binary
operator.
"""
if is_variable(root) or is_constant(root):
assert first is None and second is None
self.root = root
elif is_unary(root):
assert first is not None and second is None
self.root, self.first = root, first
else:
assert is_binary(root)
assert first is not None and second is not None
self.root, self.first, self.second = root, first, second
The main content of Chapter 1 is captured by asking you to implement various methods
and functions related to objects of class Formula. Task 2 in Chapter 1, for example, asks
you to implement the method variables() of this class, which returns a Python set of
all variable names used in the formula. The file propositions/syntax.py thus already
contains also the skeleton of this method.
propositions/syntax.py
class Formula:
..
.
def variables(self) -> Set[str]:
"""Finds all variable names in the current formula.
Returns:
A set of all variable names used in the current formula.
"""
# Task 1.2
To check that your implementation is correct, we also provide a corresponding test file,
propositions/syntax_test.py, which contains the following test:
propositions/syntax_test.py
def test_variables(debug=False):
for formula, expected_variables in [
(Formula('T'), set()),
(Formula('x1234'), {'x1234'}),
(Formula('~', Formula('r')), {'r'}),
(Formula('->', Formula('x'), Formula('y')), {'x','y'}),
..
.
(Formula(· · · ), {· · · })]:
if debug:
print('Testing variables of', formula)
assert formula.variables() == expected_variables
We encourage you to always browse through the examples within the test code before
starting to implement the task, to make sure that you fully understand any possible nuances
in the specifications of the task.
All the tests of all tasks in Chapter 1 can be invoked by simply executing the Python file
test_chapter01.py, which we also provide. The code for testing the optional tasks of
Chapter 1 is commented out in that file, so if you choose to implement any of these tasks
(which is not required in order to be able to implement any of the non-optional tasks that
follow them), simply uncomment the corresponding line(s) in that file. If you run this file
and get no assertion errors, then you have successfully (as far as we can check) solved all
of the tasks in Chapter 1.
This chapter – Chapter 0 – contains a single task, whose goal is to verify
that you have successfully downloaded our code base from the book website at
www.LogicThruPython.org, and that your Python environment is correctly set up.
task 1 Implement the missing code for the function half(x) in the file
prelim/prelim.py, which halves an even integer. Here is the skeleton of this function
as it already appears in the file:
prelim/prelim.py
Parameters:
x: even integer to halve.
Returns:
An integer `z` such that `z+z=x`.
"""
assert x % 2 == 0
# Task 0.1
$ python test_chapter00.py
Testing half of 42
Testing half of 8
$
$ python test_chapter00.py
Testing half of 42
Traceback (most recent call last):
File "test_chapter00.py", line 13, in <module>
test_task1(True)
File "test_chapter00.py", line 11, in test_task1
test_half(debug)
File "prelim/prelim_test.py", line 15, in test_half
assert result + result == 42
AssertionError
$
and implementing Task 1 with, say, return x/2 (which returns a float rather than an
int), would yield the following output:
$ python test_chapter00.py
Testing half of 42
Traceback (most recent call last):
File "test_chapter00.py", line 13, in <module>
test_task1(True)
File "test_chapter00.py", line 11, in test_task1
test_half(debug)
File "prelim/prelim_test.py", line 14, in test_half
assert isinstance(result, int)
AssertionError
$
We conclude this chapter by giving a quick overview of our journey in this book. We study
two logical formalisms: Chapters 1–6 deal with the limited propositional logic, while
Chapters 7–12 move on to the fuller (first-order) predicate logic. In each of these two
parts of the book, we take a somewhat similar arc:
Propositional Logic
In this chapter we present a formal syntax for formalizing statements within logic. Con-
sider the following example of a natural language sentence that has some logical structure:
“If it rains on Monday then we will either hand out umbrellas or rent a bus.” This sentence
is composed of three basic propositions, each of which may potentially be either true or
false: p1=“it rains on Monday”, p2=“we will hand out umbrellas”, and p3=“we will rent
a bus”. We can interpret this English-language sentence as logically connecting these three
propositions as follows: “p1 implies (p2 or p3)”, which we will write as ‘(p1→(p2|p3))’.
Our goal in this chapter is to formally define a language for capturing these types of
sentences. The motivation for defining this language is that it will allow us to precisely and
formally analyze their implications. For example, we should be able to formally deduce
from this sentence that if we neither handed out umbrellas nor rented a bus, then it did not
rain on Monday. We purposefully postpone to Chapter 2 a discussion of semantics, of the
meaning, that we assign to sentences in our language, and focus in this chapter only on the
syntax, i.e., on the rules of grammar for forming sentences.
Our language for Part I of this book is called propositional logic. While there are various
variants of the exact rules of this language (allowing for various logical operators or for
various rules about whether and when parentheses may be dropped), the exact variant used
is not very important, but rather the whole point is to fix a single specific set of rules and
stick with it. Essentially everything that we say about this specific variant will hold with
only very minor modifications for other variants as well. Here is the formal definition with
which we will stick.
definition 1.1 (Propositional Formula) The following strings are (valid1 ) proposi-
tional formulas:
• A variable name: a letter in ‘p’. . . ‘z’, optionally followed by a sequence of digits. For
example, ‘p’, ‘y12’, or ‘z035’.
• ‘T’.
• ‘F’.
• A negation ‘~φ’, where φ is a (valid) propositional formula.
1 What we call valid formulas are often called well-formed formulas in other textbooks.
13
Parameters:
string: string to check.
Returns:
``True`` if the given string is a variable name, ``False`` otherwise.
"""
return string[0] >= 'p' and string[0] <= 'z' and \
(len(string) == 1 or string[1:].isdecimal())
Parameters:
string: string to check.
Returns:
``True`` if the given string is a constant, ``False`` otherwise.
2 The decorator that precedes the definition of each of these functions in the code that you are given memoizes
the function, so that if any of these functions is called more than once with the same argument, the previous
return value for that argument is simply returned again instead of being recalculated. This has no effect on
code correctness since running these functions has no side effects, and their return values depend only on their
arguments and are immutable, but this does speed-up the execution of your code. It may seem silly to perform
such optimizations with such short functions, but this will in fact dramatically speed-up your code in later
chapters, when such functions will be called many many times from within various recursions. We use this
decorator throughout the code that you are given in various places where there are speed improvements to be
gained.
"""
return string == 'T' or string == 'F'
Parameters:
string: string to check.
Returns:
``True`` if the given string is a unary operator, ``False`` otherwise.
"""
return string == '~'
Parameters:
string: string to check.
Returns:
``True`` if the given string is a binary operator, ``False`` otherwise.
"""
return string == '&' or string == '|' or string == '−>'
Notice that Definition 1.1 is very specific about the use of parentheses: ‘(φ&ψ)’ is a
valid formula, but ‘φ&ψ’ is not and neither is ‘((φ&ψ))’; likewise, ‘~φ’ is a valid formula
but ‘(~φ)’ is not, etc. These restrictive choices are made to ensure that there is a unique and
easy way to parse a formula: to take a string that is a formula and figure out the complete
derivation tree of how it is decomposed into simpler and simpler formulas according
to the derivation rules from Definition 1.1. Such a derivation tree is naturally expressed
in a computer program as a tree data structure, and this book’s pedagogical approach is
to indeed implement it as such. So, the bulk of the tasks of this chapter are focused on
translating formulas back and forth between representation as a string and as an expression-
tree data structure.
The file propositions/syntax.py defines a Python class Formula for holding a
propositional formula as a data structure.
propositions/syntax.py
@frozen
class Formula:
"""An immutable propositional formula in tree representation, composed from
variable names, and operators applied to them.
Attributes:
root: the constant, variable name, or operator at the root of the
formula tree.
first: the first operand of the root, if the root is a unary or binary
operator.
second: the second operand of the root, if the root is a binary
operator.
"""
root: str
first: Optional[Formula]
second: Optional[Formula]
Parameters:
root: the root for the formula tree.
first: the first operand for the root, if the root is a unary or
binary operator.
second: the second operand for the root, if the root is a binary
operator.
"""
if is_variable(root) or is_constant(root):
assert first is None and second is None
self.root = root
elif is_unary(root):
assert first is not None and second is None
self.root, self.first = root, first
else:
assert is_binary(root)
assert first is not None and second is not None
self.root, self.first, self.second = root, first, second
The constructor of this class (which we have already implemented for you) takes as argu-
ments the components (between one and three) of which the formula is composed, and
constructs the composite formula. For instance, to represent the formula ‘(φ&ψ)’, the
constructor will be given the three “components”: the operator ‘&’ that will serve as the
“root” of the tree and the two subformulas φ and ψ.
example 1.1 The data structure for representing the formula ‘~(p&q76)’ is constructed
using the following code:
my_formula = Formula('~', Formula('&', Formula('p'), Formula('q76')))
The various components of my_formula from Example 1.1 can then be accessed using
the instance variables my_formula.root for the root, my_formula.first for the first
subformula (if any), and my_formula.second for the second subformula (if any). To
enable the safe reuse of existing formula objects as building blocks for other formula
objects (and even as building blocks in more than one other formula object, or as build-
ing blocks that appear more than once in the same formula object), we have defined
the Formula class to be immutable, i.e., we have defined it so that my_formula.root,
my_formula.first, and my_formula.second cannot be assigned to after my_formula
has been constructed. For example, you can verify that after my_formula is constructed as
in Example 1.1, attempting to assign my_formula.first = Formula('q4') fails. This
is achieved by the @frozen decorator that appears just before the class definition.3 Most of
the classes that you will implement as you work through this book will be made immutable
in this way.
Your first task is to translate the expression-tree representation of a formula into its
string representation. This can be done using recursion: suppose that you know how to
3 The definition of this decorator is in the file logic_utils.py that we have provided to you, and which we
imported for you into propositions/syntax.py.
convert two tree data structures formula1 and formula2 (that are both Python objects
of type Formula) into strings; how can you convert, into such a string, a tree data struc-
ture of type Formula that has '&' at its root, and formula1 and formula2 as its two
children/subformulas?
task 1 Implement the missing code for the method4 __repr__() of class Formula,
which returns a string that represents the formula (in the syntax defined in Definition 1.1).
Note that in Python, the string returned by, e.g., formula.__repr__() is also returned by
str(formula), so by solving this task you will also be implementing the functionality of
the latter.
propositions/syntax.py
class Formula:
..
.
def __repr__(self) −> str:
"""Computes the string representation of the current formula.
Returns:
The standard string representation of the current formula.
"""
# Task 1.1
class Formula:
..
.
def variables(self) −> Set[str]:
"""Finds all variable names in the current formula.
Returns:
A set of all variable names used in the current formula.
"""
# Task 1.2
4 The decorator that precedes the definition of __repr__() in the code that you are given memoizes this
method, so that any subsequent calls to this method (on the same Formula object) after the first call simply
return the value returned by the first call instead of recalculating it. This has no effect on code correctness
since the Formula class is immutable, running this method has no side effects, and the returned is immutable,
but this will dramatically speed-up your code in later chapters, when you handle complex formulas. We use
this decorator throughout the code that you are given in various places where there are speed improvements to
be gained.
task 3 Implement the missing code for the method operators() of class Formula,
which returns all of the operators that appear in the formula. By operators we mean ‘~’,
‘&’, ‘|’, ‘→’, ‘T’, and ‘F’.
propositions/syntax.py
class Formula:
.
..
def operators(self) −> Set[str]:
"""Finds all operators in the current formula.
Returns:
A set of all operators (including 'T' and 'F') used in the current
formula.
"""
# Task 1.3
1.2 Parsing
Going in the opposite direction, i.e., taking a string representation of a formula and parsing
it into the corresponding derivation tree, is usually a bit more difficult since you need to
algorithmically figure out where to “break” the complex string representation of the for-
mula into the different components of the formula. This type of parsing challenge is quite
common when dealing with many cases of formal “languages” that need to be “understood”
by a computer program, the prime example being when compilers need to understand
programs written in a programming language. There is a general theory that deals with
various classes of languages as well as algorithms for parsing them, with an emphasis
on the class of context-free languages, whose grammar can be defined by a recursive
definition. The language for formulas that we chose for this book is in this class, and is
simple enough so that a simple “recursive descent” algorithm, which we will now describe,
can handle its parsing.
The idea is to first read the first token in the string, where a token is a basic “word” of our
language: either one of the single-letter tokens 'T', 'F', '(', ')', '~', '&', '|', or the
two-letter “implies” token '−>', or a variable name like 'p' or 'q76'. This first token will
tell you in a unique way how to continue reading the rest of the string, where this reading
can be done recursively. For example, if the first token is an open parenthesis, '(', then
we know that a formula φ must follow, which can be read by a recursive call. Once φ was
recursively read, we know that the following token must be one of '&', '|', or '−>', and
once this token is read then a formula ψ must follow, and then a closing parenthesis, ')'.
This will become concrete as you implement the following task.
task 4 Implement the missing code for the static method _parse_prefix(string) of
class Formula, which takes a string that has a prefix that represents a formula, and returns
a formula tree created from the prefix, and a string containing the unparsed remainder of
the string (which may be empty, if the parsed prefix is in fact the entire string).
propositions/syntax.py
class Formula:
..
.
@staticmethod
def _parse_prefix(string: str) −> Tuple[Optional[Formula], str]:
"""Parses a prefix of the given string into a formula.
Parameters:
string: string to parse.
Returns:
A pair of the parsed formula and the unparsed suffix of the string.
If the given string has as a prefix a variable name (e.g.,
'x12') or a unary operator followed by a variable name, then the
parsed prefix will include that entire variable name (and not just a
part of it, such as 'x1'). If no prefix of the given string is a
valid standard string representation of a formula then returned pair
should be of ``None`` and an error message, where the error message
is a string with some human-readable content.
"""
# Task 1.4
The fact that given a string, the code that you wrote is able to clearly decide, without
any ambiguity, on what exactly is the prefix of this string that constitutes a valid formula,
relies on the fact that indeed our syntactic rules ensure that no prefix of a formula is also a
formula itself (with the mentioned caveat that this holds as long as a variable name cannot
be broken down so that only its prefix is taken, since, e.g., ‘x1’ is a prefix of ‘x12’). Had
our definitions been different, e.g., had we allowed ‘φ&ψ’ as a formula as well, then this
would have no longer been true. For example, under such definitions, the string 'x&y'
would have been a valid formula, and so would have its prefix 'x'. The code behind your
implementation and the reasoning of why it solves the task in the unique correct way thus
essentially prove the following lemma.
class Formula:
..
.
@staticmethod
def is_formula(string: str) −> bool:
"""Checks if the given string is a valid representation of a formula.
Parameters:
string: string to check.
Returns:
``True`` if the given string is a valid standard string
representation of a formula, ``False`` otherwise.
"""
# Task 1.5
5 The “caveat case” of a variable name as a prefix of another variable name would come up when dealing with
formulas whose first token is a variable name (rather than with a ‘(’ as in the case just detailed). In this case, to
get uniqueness we must indeed enforce that the entire variable-name token be part of the parsed prefix.
class Formula:
..
.
@staticmethod
def parse(string: str) −> Formula:
"""Parses the given valid string representation into a formula.
Parameters:
string: string to parse.
Returns:
A formula whose standard string representation is the given string.
"""
assert Formula.is_formula(string)
# Task 1.6
Our programs, like all computer programs, only handle finite data. This book however aims
to teach mathematical logic and thus needs to also consider infinite objects. We shall aim to
make a clear distinction between objects that are mathematically finite (like a single integer
number6 ) and those that can mathematically be infinite (like a set of integers) but practical
representations in a computer program may limit them to be finite. So, looking at the
definition of formulas, we see that every formula has a finite length and thus formulas are
finite objects in principle. Now, there is no uniform upper bound on the possible length of
a formula (much like there is no uniform upper bound on the possible length of an integer),
which means that there are infinitely many formulas. In particular, a set of formulas can
in principle be an infinite object: It may contain a finite number of distinct formulas or an
infinite number of distinct (longer and longer) formulas, but each of these formulas has only
a finite length. Of course, when we actually represent sets of formulas in our programs, the
represented sets will always be only of finite size.
6 Indeed, while there is no upper bound on the length of an integer number, any given single integer number is
of finite length.
As some readers may recall, in mathematics there can be different cardinalities of infi-
nite sets, where the “smallest” infinite sets are called countably infinite (or enumerable).
An infinite set S is called countably infinite, or countable, if there exists a way to list its
items one after another without “forgetting” any of them: S = {s1 , s2 , s3 , . . .}. (Formally if
there exists a function f from the natural numbers onto S.)7 The set of formulas is indeed
countable in this sense: Each formula is a finite-length string whose letters come from a
finite number of characters, and thus there is a finite number of formulas of any given fixed
length. Thus one may first list all the formulas of length 1, then those of length 2, etc. We
thus get the following simple fact.
theorem 1.2 The set of formulas is countably infinite.
While according to our definition of variable names and formulas there are only count-
ably many variable names and therefore only countably many formulas, all of the results in
this book extend naturally via analogous proofs to sets of variable names of arbitrary cardi-
nality, which imply also formula sets of arbitrary cardinality. In the few places throughout
this book where the generalization is not straightforward, we will explicitly discuss this.
The notation that we used to represent our formulas is only one possible format, and there
are other notations by which a tree data structure can be represented as a string. The
notation that we used is called infix notation since the operator at the root of the tree
is given in-between the representations of the left and right subtrees. Another commonly
used notation is polish notation.8 In this notation, the operator is printed before the (two, in
the case of a binary operator) subformulas that it operates on. Of course, these subformulas
themselves are recursively printed in the same way. In another commonly used notation,
reverse polish notation, the operator is printed after these subformulas.9 One nice advan-
tage of polish and reverse polish notations is that it turns out that parentheses are no
longer needed. Thus, for example, the formula whose regular, infix notation is ‘~(p&q76)’
would be represented in polish notation as ‘~&pq76’ and in reverse polish notation as
‘pq76&~’.
optional task 7 Implement the missing code for the static method polish() of class
Formula, which returns a string that represents the formula in polish notation.
7 For the benefit of readers who are not familiar with cardinalities of infinite sets, we note that while when first
encountering this definition it may be hard to think of any set that does not satisfy this property, in fact many
sets that you have encountered do not satisfy it. A prime example is the infinite set of all real numbers
between 0 and 1, which is not countable.
8 So called after the Polish logician Jan Łukasiewicz who invented it.
9 Polish notation and reverse polish notations are also called prefix notation and postfix notation, respectively,
analogously to infix notation, describing where the operator comes with respect to the representations of the
subtrees. We avoid these terms here in order not to confuse prefix as the name of the notation with prefix as the
word describing the beginning of a string as in “prefix-free” or as in _parse_prefix().
propositions/syntax.py
class Formula:
.
.
.
def polish(self) −> str:
"""Computes the polish notation representation of the current formula.
Returns:
The polish notation representation of the current formula.
"""
# Optional Task 1.7
class Formula:
..
.
@staticmethod
def parse_polish(string: str) −> Formula:
"""Parses the given polish notation representation into a formula.
Parameters:
string: string to parse.
Returns:
A formula whose polish notation representation is the given string.
"""
# Optional Task 1.8
In Chapter 1 we defined the syntax of propositional formulas, that is, we defined which
strings constitute valid propositional formulas. We were however careful not to assign
any meaning, any semantics, to propositional formulas. The notion of the semantics of
propositional formulas may be somewhat difficult to grasp, as the meaning of formulas may
seem “obvious” but its formal definition may at first seem elusive. This chapter provides
this formal definition.
Our intention for these semantics is as follows: Every variable name (e.g., ‘p’ or ‘q76’)
will stand for a certain primitive proposition that may be either true or false, inde-
pendently of other primitive propositions. A compound formula that contains more than
one variable name will describe a more complex proposition, whose truth or lack thereof
depends on which primitive propositions are true and which are not. For example, we may
have ‘p’ represent “It is raining,” ‘q’ represent “My umbrella is open,” ‘r’ represent “I am
singing,” and ‘s’ represent “I am dancing.” A compound formula such as ‘((p&~q)&(r|s))’
evaluates to true if it is raining and my umbrella is not open, and furthermore either I am
singing or I am dancing.
Before moving forward with the formal definition of the semantics of propositional
formulas, it may perhaps be instructive to take a short detour to another domain where
we have a distinction between syntax and semantics, a domain where we expect many of
our readers to have a good feel for semantics: programming languages.
#include <stdio.h> /*
print("wonderland")
""" */
int main() { printf("looking-glass\n"); }
// """
What would this program output? Well, since in Python comments start with the sym-
bol # and continue until the end of the line, and multiline strings (which are ignored on
their own) are enclosed between triple quotations, then graying out Python comments and
ignored strings, the program would be interpreted as follows:
24
mystery_program.py
#include <stdio.h> /*
print("wonderland")
""" */
int main() { printf("looking-glass\n"); }
// """
and when executed would simply print wonderland. This answer, as we will now explain,
while partially correct, does make some assumptions. As it turns out, the proper answer
to the question of what would the program print is “it depends on which language you
consider this program to be written in.” Indeed, this program is not only a valid program in
Python, but also in the C programming language!1 While the syntax of this program is valid
both in Python and in C, its semantics in each of these programming languages turn out
however to be completely different. In C, comments are either enclosed between /* and */,
or start with // and continue until the end of the line. Therefore, as a C program, graying
out C comments, the above program would be interpreted as follows:
mystery_program.c
#include <stdio.h> /*
print("wonderland")
""" */
int main() { printf("looking-glass\n"); }
// """
Formally, the semantics we will give to a formula are the respective truth values that it gets
in every possible setting of its variable names. We view a possible setting of these variable
names as a “possible world,” and the semantics of a formula are whether it is true or not in
each of these possible worlds. We will call such a possible world a model.
definition 2.1 (Model) Let S be the set of variable names. A model M over S is a func-
tion that assigns a truth value to every variable name in S. That is, M : S → {True, False}.
The file propositions/semantics.py, which contains all of the functions that you
are asked to implement in the next few sections, deals with the semantics of propositional
formulas. A formula is represented as an instance of the class Formula that was defined
1 Fear not if you have no familiarity with the C programming language. We will explain the little that is needed
to know about C in order to drive the point of this discussion home.
in Chapter 1. We will represent a model as a Python dict (dictionary) that maps every
variable name to a Boolean value:
propositions/semantics.py
Parameters:
model: dictionary to check.
Returns:
``True`` if the given dictionary is a model over some set of variable
names, ``False`` otherwise.
"""
for key in model:
if not is_variable(key):
return False
return True
Parameters:
model: model to check.
Returns:
A set of all variable names over which the given model is defined.
"""
assert is_model(model)
return model.keys()
Having defined a model, we can now give each formula its semantics – the truth value
that it gets in every possible model.
definition 2.2 (Truth Value of Formula in Model) Given a formula φ and a model M
over a set of variable names that contains (at least) all those used in φ, we define the (truth)
value of the formula φ in the model M recursively in the natural way:
• If φ is the constant ‘T’, its value is True; if φ is the constant ‘F’, its value is False.
• If φ is a variable name p, then its value is as specified by the model: M(p).
• If φ is of the form ‘~ψ’, then its value is True if the (recursively defined) value of ψ in
M is False (and is False otherwise).
• If φ is of the form ‘(ψ&ξ )’, then its value is True if the (recursively defined) values of
both ψ and ξ in M are True (and is False otherwise); if φ is of the form ‘(ψ|ξ )’, then
its value is True if the (recursively defined) value of either ψ or ξ (or both) in M is True
(and is False otherwise); if φ is of the form ‘(ψ→ξ )’, then its value is True if either the
(recursively defined) value of ψ in M is False or the (recursively defined) value of ξ in
M is True (and is False otherwise).
Returning to the example we started with, one possible model M is M(‘p’) = True (it
is raining), M(‘q’) = False (my umbrella is NOT open), M(‘r’) = True (I am singing),
and M(‘s’) = False (I am NOT dancing), and in this model the formula ‘((p&~q)&(r|s))’
evaluates to the value True as defined recursively: ‘~q’ evaluates to True (since ‘q’ evaluates
to False), and so ‘(p&~q)’ evaluates to True (since both ‘p’ and ‘~q’ evaluate to True); fur-
thermore, ‘(r|s)’ evaluates to True (since ‘r’ evaluates to True); and finally ‘((p&~q)&(r|s))’
evaluates to True (since both ‘(p&~q)’ and ‘(r|s)’ evaluate to True). Of course there are
more possible models, and for some of them the formula evaluates to True while for the
others it evaluates to False.
While the semantics of the not (‘~’), or (‘|’), and and (‘&’) operators are quite natural
and self-explanatory, the implies (‘→’) operator may seem a bit more cryptic. The way to
think about ‘(ψ→ξ )’ is as stating that if ψ is True, then ξ is True as well. This statement
would be False only if both ψ were True and ξ were False, so this statement is True
whenever either ψ is False or ξ is True, which coincides with Definition 2.2. Yet, it may
still intuitively seem unnatural that the statement “if ψ is True, then ξ is True as well”
is considered to be True if ψ is False (indeed, how should one interpret this conditional
if ψ is false)? The reason for this definition is that we would generally be interested in
whether a given formula is True in each of a set of models. In this context, the formula
‘(ψ→ξ )’ can be naturally interpreted as “whenever ψ is True, so is ξ ,” that is, in any
model in this set in which ψ is True, so is ξ . (This of course still does not tell us any-
thing about models in this set in which ψ is False, but replacing “if” with “whenever”
may somewhat further motivate this definition, and help make this operator a bit less
cryptic.)
Parameters:
formula: formula to calculate the truth value of.
model: model over (possibly a superset of) the variable names of the
given formula, to calculate the truth value in.
Returns:
The truth value of the given formula in the given model.
Examples:
>>> evaluate(Formula.parse('~(p&q76)'), {'p': True, 'q76': False})
True
Once we have defined the value that a formula gets in a given model, we now turn to
handling sets of possible models. If we have a set of n variable names, then there are exactly
2n possible models over this set: All possible combinations where each of the variable
names is mapped to either True or False. In the next task we ask you to list all these
possible models.
Before jumping to the task, we should explicitly note the exponential jump in the size of
the objects that we are dealing with. While all the code that you have written so far would
have no problem dealing with formulas with millions of variable names, once we want to
list all the possible models over a given set of variable names, we will not be able to handle
more than a few dozen variable names at most: already with 40 variable names we have
more than a trillion models (240 ≈ 1012 ).
task 2 Implement the missing code for the function all_models(variables), which
returns a list2 of all possible models over the given variable names.
propositions/semantics.py
Parameters:
variables: variable names over which to calculate the models.
Returns:
An iterable over all possible models over the given variable names. The
order of the models is lexicographic according to the order of the given
variable names, where False precedes True.
Examples:
>>> list(all_models(['p', 'q']))
[{'p': False, 'q': False}, {'p': False, 'q': True},
{'p': True, 'q': False}, {'p': True, 'q': True}]
2 While in this book we will not pay much attention to complexities and running times, we do pay here just a bit
of attention to this first exponential blowup. Even though for simplicity the task asks to return a list, we
recommend that readers familiar with Python iterables return an iterable that iterates over all possible models
(which does not require keeping them all together in memory) rather than actually return a list of all
possible models (which would require them to all be together in memory). The test that we provide allows for
any iterable, and not merely a list, to be returned by this function. We do not, however, intend to run any of
this code on more than a few variable names, so we do not impose any efficiency requirements on your code,
and we do allow solving this task by returning a list of all models.
Guidelines: The standard term “lexicographic order” that specifies the order of the models
refers to considering each model as a “word” in the alphabet consisting of the two “letters”
False and True, considering the “letter” False to precede the “letter” True, and listing
all the “words” (models) “alphabetically” in the sense that every word that starts with
False precedes every word that starts with True, and more generally for any prefix of
a “word,” words that start with that prefix and then False (regardless of which “letters”
follow) precede words that start with that prefix and then True (regardless of which “letters”
follow).
Hint: The product method (with its repeat argument) from the standard Python
itertools module may be useful here.
task 3 Implement the missing code for the function truth_values(formula,
models), which returns a list of the respective truth values of the given formula in
the given models.3
propositions/semantics.py
Parameters:
formula: formula to calculate the truth value of.
models: iterable over models to calculate the truth value in.
Returns:
An iterable over the respective truth values of the given formula in
each of the given models, in the order of the given models.
Examples:
>>> list(truth_values(Formula.parse('~(p&q76)'),
... all_models(['p', 'q76'])))
[True, True, True, False]
"""
# Task 2.3
We are now able to print the full semantics of a formula: its truth value for every possible
model over its variable names. There is a standard way to print this information, called a
truth table: A table with a line for each possible model, where this line lists each of the
truth values of the variable names in the model, and then the truth value of the formula in
the model.
task 4 Implement the missing code for the function print_truth_table(formula),
which prints the truth table of the given formula (according to the format demonstrated in
the docstring of this function).
3 Readers who implemented Task 2 to return a memory-efficient iterable rather than a list are encouraged to
implement this method to accept models also as an arbitrary iterable, and to also return a memory-efficient
iterable rather than a list from this function. The test that we provide allows for any iterable to be returned
by this function, but only requires the function to support taking a list of models.
propositions/semantics.py
Parameters:
formula: formula to print the truth table of.
Examples:
>>> print_truth_table(Formula.parse('~(p&q76)'))
| p | q76 | ~(p&q76) |
|---|-----|----------|
| F | F | T |
| F | T | T |
| T | F | T |
| T | T | F |
"""
# Task 2.4
We are going to pay special attention to two types of formulas – those that get the value
True in some model and those that get the value True in all models.
definition 2.3 (Satisfiable Formula; Contradiction; Tautology)
• A formula is said to be satisfiable if it gets the value True in some (at least one) model.
A formula that is not satisfiable is said to be a contradiction.
• formula is said to be a tautology if it gets the value True in all models over its variable
A
names.
For example, the formula ‘(p&~p)’ is a contradiction (why?) and thus is not satisfiable
and is certainly not a tautology, while ‘(p|~p)’ is a tautology (why?) and in particular is also
satisfiable. The formula ‘(p&q)’ is neither a contradiction nor a tautology, but is satisfiable
(why?). Note that a formula φ is a contradiction if and only if ‘~φ’ is a tautology, and thus
a formula φ is satisfiable if and only if its negation ‘~φ’ is not a tautology. One may figure
out whether a given formula satisfies each of these conditions by going over all possible
models.
task 5 Implement the missing code for the three functions is_tautology(formula),
is_contradiction(formula), and is_satisfiable(formula), which respectively
return whether the given formula is a tautology, is a contradiction, and is satisfiable.
propositions/semantics.py
Parameters:
formula: formula to check.
Returns:
``True`` if the given formula is a tautology, ``False`` otherwise.
"""
# Task 2.5a
Parameters:
formula: formula to check.
Returns:
``True`` if the given formula is a contradiction, ``False`` otherwise.
"""
# Task 2.5b
Parameters:
formula: formula to check.
Returns:
``True`` if the given formula is satisfiable, ``False`` otherwise.
"""
# Task 2.5c
All of the tasks so far accepted a formula as their input, and answered questions about its
truth value in a given model or in some set of models. In the next two tasks you are asked
to implement the “reversed” functionality: to take as input desired semantics, and output –
synthesize – a formula that conforms to it. Remarkably, this can be done for any desired
semantics.
Our first step will be to create a formula whose truth table has a single row with value
True, with all other rows having value False. This can be done in the form of a conjunctive
clause: a conjunction (i.e., a concatenation using ‘&’ operators) of (one or more) variable
names or negation-of-variable-names.
propositions/semantics.py
Parameters:
model: model over a nonempty set of variable names, in which the
synthesized formula is to hold.
Returns:
The synthesized formula.
"""
assert is_model(model)
assert len(model.keys()) > 0
# Task 2.6
Parameters:
variables: nonempty set of variable names for the synthesized formula.
values: iterable over truth values for the synthesized formula in every
possible model over the given variable names, in the order returned
by `all_models(variables)`.
Returns:
The synthesized formula.
Examples:
>>> formula = synthesize(['p', 'q'], [True, True, True, False])
>>> for model in all_models(['p', 'q']):
4 Once again, readers who implemented Task 3 to return a memory-efficient iterable rather than a list are
encouraged to implement this method to also accept values as an arbitrary iterable.
Hints: Use the function _synthesize_for_model that you implemented in Task 6. Note
that the case in which the set of models with value True is empty is a special case since we
are not allowing to simply return the formula ‘F’, so you will need to return an equivalent
DNF of your choice (over the given variable names).
The fact that you were able to complete Task 7 proves the following rather remarkable
theorem.
theorem 2.1 (The DNF Theorem) Let S be a nonempty finite set of variable names. For
every Boolean function f over the variable names in S (i.e., f arbitrarily assigns a truth
value to every tuple of truth values for the variable names in S) there exists a formula in
disjunctive normal form whose truth table is exactly the Boolean function f .
Parameters:
model: model over a nonempty set of variable names, in which the
synthesized formula is to not hold.
Returns:
The synthesized formula.
"""
assert is_model(model)
ebookgate.com