Discrete Mathematics Through Type Theory
This course is by Kevin Sullivan, University of Virginia Department of Computer Science. It is intended for second year undergraduate computer science majors. It assumes that students have had one course in general programming, e.g., in Python or Java.
Goals
The goal of this course is to provide early students of computer science with a firm grasp of formal reasoning (formal logic, set theory, properties of relations, proof theory, etc.) in a way that both leverages and resonates with their intrinsic interests in programming and computation.
Hypothesis
The hypothesis behind the design of this course is that for students in computer science, in particular, the most natural and rewarding path to understanding abtract mathematical logic and proof starts from their interest in and intuition for programing. As an example, a milestone in this course is a proof of several of DeMorgan's lawsthrough the construction of general polymorphic programs operating on product, sum, and function-typed values.
Method
This course adopts dependent type theory as an underlying framework for teaching discrete mathematics. We use the Lean prover as supported in a Docker image accessed through the VS Code IDE. The choice of Lean is based in part on its validated capacity for expressive formalization of abstract mathematics and on the fact that it's emerging as a capable general-purpose functional programming language and system, as well.
We introduce constructive reasoning by way of programming with product, sum, and function types. We cover inductive data definitions and recursive functions. On that foundation, we embed propositional logic syntax and semantics and automated semantic evaluation and SAT solving in Lean. We then gives students a fun break at the mi-term point solving puzzles in Logic using the Z3 SMT solver.
The course then shifts to teaching predicate logic using its embedding in Lean. Topics include universal and existential quantification and associated reasoning principles. New concept at this point include the Prop type universe and dependent pairs and, to encode predicates, dependent function types. We introduce tactic-based construction of terms at this point as well.
From there, on the foundation constructed so far, the course moves into proof strategies (direct, by negation, by induction), then to set theory (membership predicates) and properties of relations. For example, from the basic axioms of equality, we derive proofs of symmetry and transitivity.
Stay tuned for additional updates as the course continues through the Fall 2023 semester.
Copyright
Copyright 2023 Kevin J Sullivan. All Rights Reserved.
If you wish to use these materials, for teaching, profit, or other prupose, please contact the author, Kevin Sullivan, at sullivan@virginia.edu. He is open to others' use of these materials but at a minimum wishes to track their use.
Cite as: Sullivan, K., Discrete Mathematics Through Type Theory, 2023, currently available at URL, https://computingfoundations.org.
Lectures 1 and 2: Types, Terms, Applications
Data Types
Here are some basic data types. The #check command tells you that each of these is a Type.
BoolBool: TypeNatNat: TypeStringString: Type
Here are some terms (values) of these types. Every term in Lean has a type. #check tells you the type of any term.
true -- "literal" term of type Booltrue: Boolfalse -- another onefalse: Bool(andand: Bool → Bool → Booltruetrue: Boolfalse) -- a function application termfalse: Bool(true &&true: Boolfalse) -- using "infix" notation for "and"false: Bool
Some terms of type Nat (for "natural number")
00: Nat11: Nat22: Nat
Some terms of type String
"""": String"Logic is the best!""Logic is the best!": String(String.appendString.append: String → String → String"I love DM1""I love DM1": String"!")"!": String("I love DM1" ++"I love DM1": String"!")"!": String
Function Types
Given any two types, let's call them α and β, we can form a new type, written α → β. This is the type of functions that take an argument of type α and that return (or reduce to) a value of type, β. Note, again, that (α → β) is a type.
A function that takes a Boolean argument and that returns a Boolean result has this type.
Bool →Bool: TypeBoolBool: Type
Here's the type of function that takes two Boolean values as arguments and that and returns a Boolean value as a result.
Bool →Bool: TypeBool →Bool: TypeBoolBool: Type
Here's the type of function that takes a natural number and that returns a natural number.
Nat →Nat: TypeNatNat: Type
This is the type of any function that takes two natural numbers and returns a natural number as a result.
Nat →Nat: TypeNat →Nat: TypeNatNat: Type
A function of this type takes two string arguments and returns a string result.
String →String: TypeString →String: TypeStringString: Type
Higher-order functions
In Lean and many other functional languages, a function that takes two arguments (such as two strings) and that returns a result (say another string), can be understood instead as a function that takes only one argument and that returns a function that takes the second argument and returns a final result.
To makes sense of this statement, we just need to know that → is right-associative. Whenever we write a chain of →, elements are implicitly grouped from the right. So the type we just saw, String → String → String, is exactly the same as this type!
String → (String: TypeString →String: TypeString)String: Type
In general, a function that either returns a function as a result or that takes a function as an argument is called a higher-order function. A function that takes two arguments in a functional language such as Lean is thus a higher-order function in that it really takes one argument and returns a function that that takes the second argument and that returns a final result.
Here's the type of any function that takes two arguments, the first being a function (from String to String), the second being a String, with the function finally returning a String result.
(String →String: TypeString) →String: TypeString →String: TypeStringString: Type
As an example, a function of this type could take as a first argument a function that takes any string and adds a "!" to the end. The second argument could be a string such as "Hello." And the function would then apply the first argument (that "!" function) to the second argument, "Hello," and finally return the string "Hello!".
Function Terms
Let's check the type types of some built-in terms of these function types.
(not) -- Boolean negationnot: Bool → Bool(and) -- Boolean and (&&)and: Bool → Bool → Bool(String.append) -- Appends two stringsString.append: String → String → String(Nat.succ) -- Adds 1 to its argumentNat.succ: Nat → Nat(Nat.add) -- Returns sum of two natural numbersNat.add: Nat → Nat → Nat
Example of a higher-order function involving Strings
Consider, again, the function type, (String → String) → String → String. To repeat, a function of this type takes a function (that takes a String and returns a String), and a String, as its arguments, and returns a string as a result.
Similarly a function of type String → (String → String) can be understood either as a function that takes two String arguments and returns a String, or as a function that takes one String and returns a function, of type String → String, that takes a second argument, before finally returning (reducing to) a String result.
To make our ideas more concrete, let's analyze the String append function to see how we can view it as higher-order function: namely one that, when applied to one argument, returns a function. This will be a function that has its first argument baked in and that takes the second argument to append before returning the final result.
Binding of variable names (identifiers) to values
To begin we introduce the idea of giving (or binding) a "variable" name (aka identifier, variable name) to a term. Here we bind the names, s1 and s2, to the terms (of type String), "Hello," and "Lean!"
def s1: String
s1 := "Hello, ": String
"Hello, "
def s2: String
s2 := "Lean!": String
"Lean!"
The type of an identifier is the type of the term of which it is bound.
s1s1: Strings2s2: String
Evaluating an identifier returns its value
Evaluating a name yields the result of evaluating the term to which it is bound.
s1 -- "Hello, "s1: Strings2 -- "Lean!"s2: String(String.appendString.append: String → String → Strings1s1: Strings2) -- "Hello, Lean!"s2: String
Identifiers can be passed as arguments
We can use names to pass terms to functions, as we just saw. Here it is again.
String.appendString.append: String → String → Strings1s1: Strings2s2: String
Function application terms
Here we have a function application term, in which the string append function is applied to two arguments, s1 and s2. Evaluating a function application term reduces it to the value that the function computes given those arguments: here, to the string, "Hello, Lean!"
We can bind a name to the result of evaluating another expression, here a function application.
defs3 :=s3: StringString.appendString.append: String → String → Strings1s1: Strings2s2: Strings3s3: Strings3s3: String
Viewing String.append as a higher-order function
Now recall the type of the String.append function. We'd normally write it as String → String → String. We understand this to be the type of function that takes two string arguments and returns a result that is also a string.
The → operator is right associative, so this means that the type, String → String → String really means String → (String → String). So append really takes one string as an argument and returns a function as a result: of type String → String.
You can understand this point by seeing that function application is left associative! The following expressions should be, and are, equivalent.
String.appendString.append: String → String → String"Hello, ""Hello, ": String"Lean!""Lean!": String(String.appendString.append: String → String → String"Hello, ")"Hello, ": String"Lean!""Lean!": String
The second expression makes it clear what's really going on: First the append function consumes "Hello, " and returns a function (without a name) that then consumes the second string, "Lean!" and returns the final result, "Hello, Lean!" From now on, remember that → ("arrow") is right associative and application is left associative.
So what does the weird, unnamed intermediate function, (String.append "Hello, ") do? It appends "Hello, " and whatever argument is receives: in this example, "Lean!", and returns the final result.
Putting all these ideas together, we should be able to apply append to one string and get ourselves a function (of type String → String), then bind a name to it, and finally apply that function to another string argument! Yes, it actually works! Recall that s1 here is the string, "Hello, ".
deff1 :=f1: String → StringString.appendString.append: String → String → Strings1 -- "Hello, " is baked in to f1s1: String(f1) -- f1 is a function of type String → Stringf1: String → String
Whoa, so f1 is a some function that takes just one string as an argument and that returns "Hello, " (which is now "baked into" f1) and whatever second string value s2 has.
f1f1: String → String"Lean!" -- "Hello, Lean!""Lean!": Stringf1f1: String → String"Mary!" -- "Hello, Mary!""Mary!": Stringf1f1: String → String"Joe!" -- "Hello, Joe""Joe!": String
Some more examples
All multi-argument functions are evaluated in the same way in Lean: a function consumes its first argument and returns a function that consumes its second argument and returns a function that consumes its ... until you get to the end of the chain of arguments at which point you get a value: either a function or just a data value. For example, the natural number addition function works in the same way.
Nat.addNat.add: Nat → Nat → Nat22: Nat5 -- 7 def5: Natadd2 :=add2: Nat → NatNat.addNat.add: Nat → Nat → Nat2 -- a function that adds 2 to any Nat!2: Natadd2add2: Nat → Nat5 -- 75: Natadd2add2: Nat → Nat10 -- 1210: Natadd2add2: Nat → Nat15 -- 1715: Nat
A self-test
To see if you've gotten it, consider these three function types and answer the following questions.
(String →String: TypeString) →String: TypeString -- #1String: TypeString → (String: TypeString →String: TypeString) -- #2String: TypeString →String: TypeString →String: TypeString -- #3String: Type
Questions:
- Which two types are equivalent?
- Are #1 and #3 equivalent?
- Give English explanations of these function types
- Give some examples of functions of these types
Functions that take functions as arguments
Here's a function that takes two arguments, f and a, where f is a function taking a string and returns a string, where a is a string, and where the result is a string obtained by applying f to a.
def crazy: (String → String) → String → String
crazy (f: String → String
f : String: Type
String → String: Type
String) (a: String
a : String: Type
String) : String: Type
String := (f: String → String
f a: String
a)
The type of this function is (String → String) → String → String. If we call the first argument f and the second a, this function then returns the result of applying f to a, written as (f a).
(crazy) -- (String → String) → String → Stringcrazy: (String → String) → String → String
Note that f1 as defined previously is a function that takes and returns a string, so f1 can be used as a first argument to crazy.
crazycrazy: (String → String) → String → Stringf1f1: String → Strings1 -- Results in application of f1 to s1s1: String
Self-test
Question: What is the type of the crazy function? Be careful. How can you check if your answer is correct? (Ok, yeah, I've already given you the answer.)
Function definition syntax in Lean
Important detail. The preceding definition of crazy uses a Java-ish syntax to define the function type. It explains that the first argument, f, is a function; the second, a, is a string; the return value is String; and the actual value returned is computed by applying f to a.
There's another syntax in Lean that we can use to define the same function. It's nice because the function type is clearer in this notation.
def crazy2: (String → String) → String → String
crazy2 : (String: Type
String → String: Type
String) → String: Type
String → String: Type
String
| f: String → String
f, a: String
a => (f: String → String
f a: String
a)
On the first line we declare the type of the crazy function (here called crazy2). On the second line, to the left of the => we bind names to the arguments of the function; and to the right of the => we provide an expression that computes the return value.
Self-test
What does the following expression evaluate to? Answer before using Lean to compute it for you. Recall that f1 is the function defined above that prepends "Hello, " to its argument, and s2 is the string, "Lean!".
crazy2crazy2: (String → String) → String → Stringf1f1: String → Strings2s2: String
Good. The crazy2 function applies f1 to s2 yielding the string, "Hello, Lean!" (again).
Defining our own (Boolean) functions
Let's now turn to the question of how to define our own functions more generally. To provide motivation, we'll observe that Lean already provides definitions of the Boolean functions, not, and, and or, but not of xor, nand, or nor.
Here are the names of three built-in Boolean functions in Lean. You might know them as !, &&, and || from your first programming class.
(not)not: Bool → Bool(and)and: Bool → Bool → Bool(or)or: Bool → Bool → Bool
We can confirm that these functions behave as expected
notnot: Bool → Booltrue -- falsetrue: Boolnotnot: Bool → Boolfalse -- truefalse: Boolandand: Bool → Bool → Booltruetrue: Booltrue -- truetrue: Boolandand: Bool → Bool → Booltruetrue: Boolfalse -- falsefalse: Boolandand: Bool → Bool → Boolfalsefalse: Booltrue -- falsetrue: Boolandand: Bool → Bool → Boolfalsefalse: Boolfalse -- falsefalse: Booloror: Bool → Bool → Booltruetrue: Booltrue -- truetrue: Booloror: Bool → Bool → Booltruetrue: Boolfalse -- truefalse: Booloror: Bool → Bool → Boolfalsefalse: Booltrue -- truetrue: Booloror: Bool → Bool → Boolfalsefalse: Boolfalse -- falsefalse: Bool
Not all Boolean functions are built-in
But xor, nor, and nand are not defined
#check () #check () #check ()
We can define functions ourselves
We can use the second style of function definition (from above) to define the xor function. Recall that (xor b1 b2) is true when either b1 or b2 is true but it is false if both b1 and b2 are either true or false.
The first line of the following definition specifies the name and type of the function we're defining. Each of the next four lines defines how the function behaves by cases. The first line, for example, says if the first argument (to which xor is applied) is true and the second argument is true then the xor function will return true. The remaining lines give answers for the other three cases of possible input pairs.
defxor :xor: Bool → Bool → BoolBool ->Bool: TypeBool ->Bool: TypeBool |Bool: Typetrue,true: Booltrue =>true: Boolfalse |false: Booltrue,true: Boolfalse =>false: Booltrue |true: Boolfalse,false: Booltrue =>true: Booltrue |true: Boolfalse,false: Boolfalse =>false: Boolfalsefalse: Boolxorxor: Bool → Bool → Booltruetrue: Booltrue -- falsetrue: Boolxorxor: Bool → Bool → Booltruetrue: Boolfalse -- truefalse: Boolxorxor: Bool → Bool → Boolfalsefalse: Booltrue -- truetrue: Boolxorxor: Bool → Bool → Boolfalsefalse: Boolfalse -- falsefalse: Bool
Self-tests
The nand function, short for "not and" gives exactly the opposite of the answer that the and function gives in each case. Self-test: Fill in the correct output values for this function.
defnand :nand: Bool → Bool → BoolBool ->Bool: TypeBool ->Bool: TypeBool |Bool: Typetrue,true: Booltrue =>true: Bool|true,true: Boolfalse =>false: Bool|false,false: Booltrue =>true: Bool|false,false: Boolfalse =>false: Bool
Complete this definition of the nor (not or) function. It must return the opposite of what the or function returns in each case.
defnor :nor: Bool → Bool → BoolBool ->Bool: TypeBool ->Bool: TypeBool :=Bool: Type-- delete this line and fill in the four cases
Suppose that a function takes two Boolean inputs and returns the "conjunction" (and) of the "negation" (not) of each argument. Is this the same function as one we have already discussed? Which one? Use #eval if you need to to figure out its value for each combination of input values.
def mystery : Bool -> Bool -> Bool
| b1, b2 => and (not b1) (not b2)
Pattern matching
Now there's something perplexing going on here that needs explanation. Look at the definition of nand above and the definition of mystery here. In the first (nand) example, the cases "match" possible values of the arguments, e.g., if the first is true and the second is false then ... The key observation is that in this example, we're matching on already defined values.
In the case of the mystery function, on the other hand, b1 and b2 are not defined when they appear in the single rule for evaluating this function. Here these names become bound to the function arguments so that the result value can be expressed in terms of these now named argument values.
In the following "application" for example, b1 is bound to true, b2 is bound to false, and in this context, the return result is defined to be the value of the expression, and (not b1) (not b2). That in turn is and false true. And that expression then evaluates to false, which is the final result of applying the mystery function to these arguments.
mysterymystery: Bool → Bool → Booltruetrue: Boolfalse -- false -- b1 b2 (!b1 && !b2)false: Bool
Remember, an undefined identifier matches with and becomes bound to any value of the corresponding argument to a function, while defined values match only when the argument values are the same. It's a little more complicated than that in general but not much.
Finally, a rule in Lean and similar proof assistants is that functions have to have defined return values for all possible combinations of their argument values. If you leave out one or more cases, Lean will give you an error message according.
deferror_example :error_example: Bool → BoolBool →Bool: TypeBoolBool: Type-- error, missing case for false
Abstract and Concrete Syntax
We've written application expressions, such as (Nat.add 1 2) placing the function name before its arguments. This we can call "abstract" syntax. In everyday paper-and-pencil mathematics we usually shorten function names to symbols and when a function takes two arguments, we put the symbol in between the arguments.
This is called "concrete" syntax: in particular using "infix" notation. In some case, we write a concrete symbols before its single argument, as in !true. That is called prefix notation. In some cases, we write a symbol after its single argument, as in 10! (ten factorial). That is called postfix notation. But in all cases henceforth, you should understand that all such expressions just represent applications of functions to given arguments. Lean simply translates concrete syntax into abstract syntax as a first step in evaluating such expressions.
!true -- prefix notation for nottrue: Bool!falsefalse: Booltrue &&true: Booltrue -- infix notation for andtrue: Booltrue &&true: Boolfalsefalse: Boolfalse &&false: Booltruetrue: Boolfalse &&false: Boolfalsefalse: Booltrue ||true: Booltrue -- infix notation for ortrue: Booltrue ||true: Boolfalsefalse: Boolfalse ||false: Booltruetrue: Boolfalse ||false: Boolfalsefalse: Bool0 +0: Nat0 -- infix notation for Nat.add0: Nat"Hello" ++"Hello": String", Logic!" -- infix notation for String.append", Logic!": String
On the ambiguity of Natural language
Consider a warning sign on escalator: "Shoes must be worn; Dogs must be carried." How many different meanings could you possibly attach to this command? Be creative.
Now consider what the words "and" and "or" could mean, in English. -- Example 1: they got married and they had a baby -- Example 2: they had a baby and they got married -- Example 3: You can have a candy or you can have a donut
The first two examples illustrate a meaning for "and" that involves some notion of temporal ordering. On the other hand, in the propositional and predicate logic we'll study, "and" has no such sense, but is true if and only if both arguments are true. The formal definition of and as a function in the same style as we defined nand makes its meaning unambiguous.
In the third example, the dad almost certainly meant that you can have one or the other but not both. In the propositional and predicate logics we'll study, an or expression is true if either or both of its arguments are true. The exclusive or (xor) function, on the other hand, is false when both inputs are true. Is xor what the dad meant?
Did the dad mean that you can have one or the other but not both and that you must have at least one? That'd be xor, again. But he probably didn't really mean that she had to have at least one sweet. What he really meant in all likelihood was that it'd be okay ("true") for her to have none, or one, or the other, but not both. What logical function captures that idea precisely? Hint: Compare the output of this function for each case with the outputs of the or function. How do they relate?
The ambiguity of natural language is resolved by giving "formal," which is to say mathematical, definitions of terms such as and and or. And once our informal ideas are represented formally, we can then apply the amazing tools of logic and mathematics to reason about them very precisely.
Self-test
Self test: Which mathematical function captures the, most plausible interpretation of the snack policy that the Dad was communicating to his daughter? (You can have one or the other or none but not both)?
namespace lecture_03_04
Note: These notes are not yet complete. They current extend the material presented at the end of lecture 3, August 29, 2023. This section will be extended as we go forward.
Generalization and Specialization
Consider the following three functions.
def id_nat: Nat → Nat
id_nat : Nat: Type
Nat → Nat: Type
Nat
| n: Nat
n => n: Nat
n
def id_string: String → String
id_string : String: Type
String → String: Type
String
| n: String
n => n: String
n
def id_bool: Bool → Bool
id_bool : Bool: Type
Bool → Bool: Type
Bool
| n: Bool
n => n: Bool
n
Each one returns the value of its single argument. We call such function identity functions. We thus have identity functions for arguments of types Nat, String, and Bool, respectively. Here are example applications.
id_natid_nat: Nat → Nat77: Natid_stringid_string: String → String"Hello""Hello": Stringid_boolid_bool: Bool → Booltruetrue: Bool
Beyond having different names, these functions vary only in the types of their argument and return values.
We wouldn't want to have to write one such function for each of hundreds of types. We can avoid such repetition by "factoring out" the varying part of the definition into a parameter (argument).
Parametric Polymorphism
A key idea throughout computer science and mathematics is that we can generalize families of definitions by turning aspects that vary into parameters. Then by giving specific values for parameters, we recover the specialied versions.
In the cases above, the main aspect that varies is the type of objects that are being handled: Bools, Strings, Nats. The code for each implementation is identical so we should really only have to write it once, in a general way, using the idea of generalization.
To do this, we introduce a new argument: one that can take on any type value whatsoever. We could call this argument, T : Type, but in Lean it's conventional to use lower-case Greek letters to name type-valued arguments, so we'll call it α : Type. Here's the code we want.
def id_poly: (α : Type) → α → α
id_poly : (α: Type
α : Type: Type 1
Type) → α: Type
α → α: Type
α
| _, v: x✝
v => v: x✝
v
def id_poly': (α : Type) → α → α
id_poly' (α: Type
α : Type: Type 1
Type) : α: Type
α → α: Type
α
| v: α
v => v: α
v
The key idea in play here is that we bind a name, α, to the value of the (first) type parameter, and, having done that, we then express the rest of the function type in terms of α. In more detail, here are the elements of the whole function definition:
- def is the keyword for giving a definition
- id_poly is the name of the function being defined
- (α : Type) binds the name α to the first (type) argument
- in this context, the rest of the function type is α → α
- the | gives the pattern matching rule for this function
- the names α and v bind to the first and the second arguments
- => separates the pattern on the left from the return value on the right
- v, bound to the second argument, is the return value of this function
- the name α is unused after the => and so can be replaced by _
-- And we can see that it works!(id_polyid_poly: (α : Type) → α → αString)String: Type"Hello!""Hello!": String(id_polyid_poly: (α : Type) → α → αNat)Nat: Type77: Nat(id_polyid_poly: (α : Type) → α → αBool)Bool: Typetruetrue: Bool
Specialization by (partial) application
For example, if α is Nat, the rest of the function is of type Nat → Nat. In the single pattern matching rule, we bind v to the first unnamed argument, a Nat, and the function then returns the value of v. If α is String, v will be bound to a String given as a second argument, and the function will return that value.
(id_poly) -- generalized definitionid_poly: (α : Type) → α → α(id_polyid_poly: (α : Type) → α → αNat) -- specialization to NatNat: Type(id_polyid_poly: (α : Type) → α → αBool) -- specialization to BoolBool: Type(id_polyid_poly: (α : Type) → α → αString) -- specialization to StringString: Type
We can specialize the generalized function to specific types by applying it only to a first type argument.
defid_nat' :=id_nat': Nat → Natid_polyid_poly: (α : Type) → α → αNat -- same as id_nat above defNat: Typeid_string' :=id_string': String → Stringid_polyid_poly: (α : Type) → α → αString -- same as id_string above defString: Typeid_bool' :=id_bool': Bool → Boolid_polyid_poly: (α : Type) → α → αBool -- same as id_bool aboveBool: Typeid_nat'id_nat': Nat → Nat77: Natid_string'id_string': String → String"Hello""Hello": Stringid_bool'id_bool': Bool → Booltruetrue: Bool
What we see here is an example of what, in programming, is called parametric polymorphism. We have one function definition that can take arguments of many different types. Here the types of the second argument and return value are given by the value (a type!) of the first argument.
Lean detects type errors in such expressions. For example, if we pass Bool as the first argument but 7 as the second, Lean will report an error. Let's try.
id_polyid_poly: (α : Type) → α → αBoolBool: Type-- Lean can't convert 7 into a Boolid_polyid_poly: (α : Type) → α → αBoolBool: Typetruetrue: Boolid_polyid_poly: (α : Type) → α → αNatNat: Type77: Natid_polyid_poly: (α : Type) → α → αStringString: Type"Hello""Hello": String
Implicit Arguments
You might have noticed that in principle Lean can always infer the type value of the first argument to the id_poly function from the data value passed as the second argument. For example, if the second argument is "Hello!", the first argument just has to be String. If the second argument is 7, the first has to be Nat. If the second is true, the first has to be Bool.
In these cases, you can ask Lean to silently fill in argument values when it knows what they must be, so that you don't have to write them explicitly. To tell Lean you want it to infer the value of the first type argument to id_poly, you specify it as an argument when defining the function not using (α : Type) but using curly braces instead: {α : Type}. Let's define the function again (with the name id_poly') to see this idea in action.
def id_poly'': {α : Type} → α → α
id_poly'' : {α: Type
α : Type: Type 1
Type} → α: Type
α → α: Type
α -- α is an implicit argument
| _, v: x✝
v => v: x✝
v
Now we can write applications of id_poly' without giving the first (type) argument explicitly. It's there, but we don't have to write it. Instead, Lean infers what it's value must be from context: specifically from the type of the value we pass as the second argument. The resulting code is beautifully simple and evidently polymorphic. It also eliminates possible type mismatches between the first and second arguments, as the type in question is inferred automatically from the value to be returned.
id_poly''id_poly'': {α : Type} → α → α7 -- α = Nat, inferred!7: Natid_poly''id_poly'': {α : Type} → α → α"Hello!" -- α = String, inferred!"Hello!": Stringid_poly''id_poly'': {α : Type} → α → αtrue -- α = Bool, inferred! #evaltrue: Bool-- error #eval-- error #eval-- error
Sometimes we will have to give type arguments explicitly, even when they're declared to be implicit. In these cases, we disable implicit argument inference, In Lean, by writing an @ before the given expression. Note that in the following examples we once again can, and must, give the type argument values explicitly.
@id_poly''id_poly'': {α : Type} → α → αNatNat: Type7 -- α = Nat, inferred!7: Nat@id_poly''id_poly'': {α : Type} → α → αStringString: Type"Hello!" -- α = String, inferred!"Hello!": String@id_poly''id_poly'': {α : Type} → α → αBoolBool: Typetrue -- α = Bool, inferred!true: Bool
Extended Example: A polymorphic apply2 function
We'll now work up to defining a polymorphic function, apply2, that takes as its arguments a function, f, and a value, a, and that returns the result of applying f to a twice: that it, it returns the value of f (f a).
A Natty Example
We'll define apply2_nat as a function that takes a function, f, and an argument, a, to that function as its arguments, and that then returns the result of applying the function f to the argument a twice. That is, apply will return the value of f (f a).
As an example, if f is the function, Nat.succ, that returns one more than a given natural number a, the result of "applying f twice to 0" is 2.
Let's write this apply2_nat function where the function and its argument values are Natty. We define apply2_nat that takes (1) a function, f : Nat → Nat, and (2) a second argument, a : Nat, and that returns a result of applying f twice to a: namely f (f a), also a Nat.
-- This apply2 version is specialized for "Natty" values f a
def apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat : (Nat: Type
Nat → Nat: Type
Nat) → Nat: Type
Nat → Nat: Type
Nat
| f: Nat → Nat
f, a: Nat
a => f: Nat → Nat
f (f: Nat → Nat
f a: Nat
a)
Let's apply this function to some arguments to see what we get. First we need some specific function, f, taking and returning a Nat. The Nat.succ function will work. This is the successor function, which returns 1 more than any natural number given as an argument.
(Nat.succ) -- Nat → NatNat.succ: Nat → NatNat.succNat.succ: Nat → Nat0 -- 10: Natapply2_natapply2_nat: (Nat → Nat) → Nat → NatNat.succNat.succ: Nat → Nat0 -- expect 20: Natapply2_natapply2_nat: (Nat → Nat) → Nat → NatNat.succNat.succ: Nat → Nat3 -- expect 53: Nat
Yay, it seems to work. It gets more interesting when we see that we can use any function of type Nat → Nat as a first argument to this function. Here are a few little puzzles for you to complete by defining simple functions.
First, define a function, double : Nat → Nat that returns twice the argument to which it's applied. So for example, double 4 should reduce to 8.
defdouble :double: Nat → NatNat →Nat: TypeNat |Nat: Typen =>n: Nat2 *2: Natnn: Natapply2_natapply2_nat: (Nat → Nat) → Nat → Natdoubledouble: Nat → Nat4 -- expect 16 (2 * (2 * 4))4: Natapply2_natapply2_nat: (Nat → Nat) → Nat → Natdoubledouble: Nat → Nat10 -- expect 40 (2 * (2 * 10))10: Nat
Second, define a function, square : Nat → Nat, that reduces to its argument value squared. Then check to see that apply2_nat works when you give square as the first argument? For example squaring 5 gives 25, and squaring 25 gives 625, so apply2_nat square 5 should reduce to 625. Write both the function definition and test cases for a few inputs, including 5. Give your answer here:
#A. define the square function here:
-- here: defsquare :square: Nat → NatNat →Nat: TypeNat |Nat: Typen =>n: Natn^n: Nat22: Natsquaresquare: Nat → Nat4 -- expect 164: Nat
Write test cases for apply2_nat square for several values, including 5, and use them to develop confidence that your function definition appears to be working more generally.
apply2_natapply2_nat: (Nat → Nat) → Nat → Natsquaresquare: Nat → Nat1 -- expect 11: Natapply2_natapply2_nat: (Nat → Nat) → Nat → Natsquaresquare: Nat → Nat2 -- expect 162: Natapply2_natapply2_nat: (Nat → Nat) → Nat → Natsquaresquare: Nat → Nat3 -- expect 813: Natapply2_natapply2_nat: (Nat → Nat) → Nat → Natsquaresquare: Nat → Nat4 -- expect 2564: Nat
A Stringy Example
Now if you think about it, we should be able to write an apply2 function that does the analogous thing but with Stringy things. Given a function, f, from String to String, and an argument, a : String, we can always compute f (f a ).
Your new puzzle is to write apply2_string; then give examples of applying this function to two different function arguments, and for each of those, to several string argument values.
You can make up your own String → String functions. For example, a function, exclaim : String → String, applied to a string, s, could return (append s "!"). There is an infix notation: s ++ "!".
defexclaim :exclaim: String → StringString →String: TypeString |String: Types =>s: Strings ++s: String"!""!": Stringexclaimexclaim: String → String"Hello" -- apply it once"Hello": Stringexclaim (exclaim: String → Stringexclaimexclaim: String → String"Hello") -- apply it twice"Hello": String
Now you can use this function, exclaim, as a first argument to apply2_string. Defining this function is easy, as it's the same as apply2_nat except for the type of objects being handled: String not Nat.
defapply2_string : (apply2_string: (String → String) → String → StringString →String: TypeString) →String: TypeString →String: TypeString |String: Typef,f: String → Stringa =>a: Stringf (f: String → Stringff: String → Stringa)a: Stringapply2_stringapply2_string: (String → String) → String → Stringexclaimexclaim: String → String"Hello" -- expect "Hello!!""Hello": String
It works!
Generalizing the Type of Objects Handled
At this point it should be clear, by analogy with earlier material, that we can generalize from the specific Nat and String types, in the previous examples, to write a version of apply2 that can handle objects of any type, α. The trick, as usual, is to handle the variation in object types by adding a type parameter.
defapply2' : (apply2': (α : Type) → (α → α) → α → αα :α: TypeType) → (Type: Type 1α →α: Typeα) →α: Typeα →α: Typeα | _,α: Typef,f: x✝ → x✝a =>a: x✝f (f: x✝ → x✝ff: x✝ → x✝a) /- Let's explain this function in detail: - def is the keyword for binding names to values - apply2' is the name of our new function - the type of the function is give after the : - the function takes three arguments: - a type value, α, such as Nat or String - a function of type α → α, such as exclaim - a value of type α - next is rule for computing the result - first we match all three arguments - the type value (we don't have to name it) - the function (we name it f) - the argument (we name it a) - after the => is the expression for the result We can now try it out to see that it works! -/a: x✝apply2'apply2': (α : Type) → (α → α) → α → αNatNat: TypeNat.succNat.succ: Nat → Nat0 -- expect 20: Natapply2'apply2': (α : Type) → (α → α) → α → αNatNat: Typedoubledouble: Nat → Nat1 -- expect 41: Natapply2'apply2': (α : Type) → (α → α) → α → αNatNat: Typesquaresquare: Nat → Nat2 -- expect 162: Natapply2'apply2': (α : Type) → (α → α) → α → αStringString: Typeexclaimexclaim: String → String"Hello" -- "Hello!!""Hello": String
Type Inference and Implicit Arguments
As a final exercise in good notation, redefine apply2 (calling it apply2') so that the first argument, the type value, is implicit. Write the definition so that Lean infers the value of α (the first, type, argument) from the values of the remaining arguments. When you get it right, the following test cases should work.
-- Answer: defapply2 : {apply2: {α : Type} → (α → α) → α → αα :α: TypeType } → (Type: Type 1α →α: Typeα) →α: Typeα →α: Typeα | _,α: Typef,f: x✝ → x✝a =>a: x✝f (f: x✝ → x✝ff: x✝ → x✝a) -- Now the type arguments are implicit!a: x✝apply2apply2: {α : Type} → (α → α) → α → αNat.succNat.succ: Nat → Nat0 -- expect 20: Natapply2apply2: {α : Type} → (α → α) → α → αdoubledouble: Nat → Nat1 -- expect 41: Natapply2apply2: {α : Type} → (α → α) → α → αsquaresquare: Nat → Nat2 -- expect 162: Natapply2apply2: {α : Type} → (α → α) → α → αexclaimexclaim: String → String"Hello" -- Hello!!"Hello": String
This example is an important achievement. It exhibits the following fundamental concepts:
- every value has a type
- types are values too; their type is Type
- types parameters make definitions polymorphic
- type arguments can be implicit and inferred
- functions are values, too, and can be arguments
With all the work required to get to this point now in hand, we're ready to introduce a new and important concept in mathematics.
Note: The concept is introduced as a homework assignment, then reviewed in class. Once it's done, this lecture then continues to completion.
end lecture_03_04Function Composition
We can now highlight first great generalization of this course: a function, we call it compose, that combines any two compatible functions, g and f, into a new function, denoted (g ∘ f), and pronounced g after f, where for any compatible argument, a, (g ∘ f) a is defined as g (f a). By compatible we mean that (1) a is the input type of f, and (2) the output type of f is the input type of g. When this is the case, we can provide a as an input to f and (f a) as an input to g to compute g (f a) as a final result. In this chapter we build up from where we left off in the last chapter to a precise and general mathematical definition of function composition.
Given f : α → α, apply2 returns (f ∘ f)
We start by further analyzing our polymorphic apply2 function from the last chapter. Here it is again.
def apply2: {α : Type} → (α → α) → α → α
apply2 {α: Type
α : Type: Type 1
Type} : (α: Type
α → α: Type
α) → α: Type
α → α: Type
α
| f: α → α
f, a: α
a => f: α → α
f (f: α → α
f a: α
a)
Given any type, α (implicitly), apply2 takes a function, f : α → α, and a value a : α, as its arguments and returns the value of f (f a).
But given our definition of function composition, we can see that this result can also be written as (f ∘ f) a: as we just defined (f ∘ f) a* to be nothing other than (f (f a)). Revisit the first paragraph of this chapter to be sure that's clear.
As an aside, here, again, are a few simple functions from the last chapter, again, for use in examples to follow.
def double: Nat → Nat
double (n: Nat
n : Nat: Type
Nat) := 2: Nat
2 * n: Nat
n
def square: Nat → Nat
square (n: Nat
n : Nat: Type
Nat) := n: Nat
n ^ 2: Nat
2
def exclaim: String → String
exclaim (s: String
s : String: Type
String) := s: String
s ++ "!": String
"!"
def is_even: Nat → Bool
is_even (n: Nat
n : Nat: Type
Nat) := n: Nat
n % 2: Nat
2 == 0: Nat
0
Now consider a simple application of apply2. Before reading any further, be sure that you fully understand what it computes and how.
apply2apply2: {α : Type} → (α → α) → α → αdoubledouble: Nat → Nat5 -- (double (double 5) -- (double ∘ double) 5 -- expect 205: Nat
Now consider how this expression is evaluated. Remember: function application is left associative. The expression, apply2 double 5, is thus evaluated as (apply2 double) 5. The expression (apply2 double) returns a function that then takes a next argument, such as 5, which it then doubles twice. Be sure you see that (apply2 double) is a function.
apply2apply2: {α : Type} → (α → α) → α → αdoubledouble: Nat → Nat5 -- expect 205: Nat(apply2apply2: {α : Type} → (α → α) → α → αdouble)double: Nat → Nat5 -- exactly the same5: Nat
Now a key question: what function is (apply2 double)? Well, it's the function that, when applied to an argument, a, applies double to it and then applies double to that result. It's thus exactly the function, (double ∘ double). Let's bind the name double_after_double to this function.
defdouble_after_double := (double_after_double: Nat → Natapply2apply2: {α : Type} → (α → α) → α → αdouble) -- Sure enough it's a function from Nat to Natdouble: Nat → Nat(double_after_double) -- And it *behaves* just as expecteddouble_after_double: Nat → Natdouble_after_doubledouble_after_double: Nat → Nat0 -- expect 0 (double (double 0))0: Natdouble_after_doubledouble_after_double: Nat → Nat1 -- expect 4 (double (double 1))1: Natdouble_after_doubledouble_after_double: Nat → Nat5 -- expect 20 (double (double 5))5: Nat
Leaving the final argument to be provided later, we see that (apply2 double) is (double ∘ double). More generally, given any type, α, apply2 applied to any function, f : α → α, returns the function, f ∘ f: f composed with itself.
defsquare_after_square :=square_after_square: Nat → Natapply2apply2: {α : Type} → (α → α) → α → αsquare -- square ∘ squaresquare: Nat → Natsquare_after_squaresquare_after_square: Nat → Nat5 -- expect 625 def5: Natexclaim_after_exclaim :=exclaim_after_exclaim: String → Stringapply2apply2: {α : Type} → (α → α) → α → αexclaim -- exclaim ∘ exclaimexclaim: String → Stringexclaim_after_exclaimexclaim_after_exclaim: String → String"Love math" -- "Love math!!""Love math": String
Generalizing to Functions of Different Types
Of course, apply2 is a pretty limited mechanism for composing functions: it can only compose a function, f : α → α, with itself, to return (f ∘ f). As you saw on the homework, we can also glue functions with different types together, as long as the output type of one is the same as the input type of the second.
On the homework, we worked up to defining glue_funs as a polymorphic function that, given any three types, α, β, and γ, takes two functions, g : β → γ and f : α → β along with any argument a : α and that returns (g (f a)), which we now understand to be (g ∘ f) a.
def glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs {α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type} :
(β: Type
β → γ: Type
γ) → -- type of g
(α: Type
α → β: Type
β) → -- type of f
α: Type
α → -- type of a
γ: Type
γ -- result type
| g: β → γ
g, f: α → β
f, a: α
a => g: β → γ
g (f: α → β
f a: α
a)
Let's see an easy example. In this example,
- α is String
- β is Nat
- γ is Bool
-- We need a function f : String → Nat deflen :len: String → NatString →String: TypeNat :=Nat: TypeString.lengthString.length: String → Nat(len) -- String → Nat -- We need a function of type Nat → Bool deflen: String → Natev (ev: Nat → Booln :n: NatNat) :Nat: TypeBool :=Bool: Typen%n: Nat2=2: Nat00: Nat(ev) -- Nat → Bool -- glue_funs composes ev and len into a String → Bool function!ev: Nat → Boolglue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γevev: Nat → Boollenlen: String → Nat"Hello" -- expect false -- Remember application is left associative"Hello": String(glue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γevev: Nat → Boollen)len: String → Nat"Hello" -- expect false -- (glue_funs ev len) is the function we want!"Hello": String(glue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γevev: Nat → Boollen) -- String → Bool -- Applied to a String it gives back a Bool!len: String → Nat(glue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γevev: Nat → Boollen)len: String → Nat"Hello!" -- expect true -- We can even name this function then use it. def"Hello!": Stringev_string := (ev_string: String → Boolglue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γevev: Nat → Boollen)len: String → Natev_stringev_string: String → Bool"Hi!" -- expect false"Hi!": Stringev_stringev_string: String → Bool"Hello" -- expect false"Hello": Stringev_stringev_string: String → Bool"" -- expect true"": Stringev_stringev_string: String → Bool"I Love Logic" -- true"I Love Logic": String
Wow. So glue_funs is in essence a function for gluing together two functions into a new function, where the input of one is the output of the other, given a value to which the whole thing is applied.
Recall that just as with apply2, leaving off the third argument, a, to glue_funs, we will return exactly the function, (g ∘ f). That is, we'll get the function that, when applied to an argument, (a : α), return (g (f a)). What function does that? It's just (g ∘ f). Pronounce this function as g after f. The idea is that it applies g after (to the result of) applying f to a.
glue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γsquaresquare: Nat → Natdoubledouble: Nat → Nat4 -- expect 64 -- Apply glue_funs to first two arguments def4: Natsquare_after_double :=square_after_double: Nat → Natglue_funsglue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γsquaresquare: Nat → Natdouble -- Apply the resulting function to 4double: Nat → Natsquare_after_doublesquare_after_double: Nat → Nat4 -- (square ∘ double) 4 -- square (double 4) -- square 8 -- 644: Nat
The square_after_double function is (square ∘ double). Indeed, you even pronounce (square ∘ double) as square after double. That makes sense because when you apply (square ∘ double) to an argument a you apply square after you apply double to a.
Self-test: What is the type of the function returned by glue_funs when applied to two function arguments, g : β → γ and f : α → β?
Insight: glue_funs is a general function composition operation! A better name for it is compose! We'll start by calling it compose' and then make some improvements.
In this firt definition we've added explicit parentheses around (α → γ). As → is right associative, doing this leaves the meaning unchanged from above. This is just an exact repeat of glue_funs. But with parentheses, the type of compose' reads better: it takes two functions, of types (β → γ) and (α → β), and returns a function of type (α → γ). That's it!
def compose': {α β γ : Type} → (β → γ) → (α → β) → α → γ
compose' {α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type} :
(β: Type
β → γ: Type
γ) →
(α: Type
α → β: Type
β) →
(α: Type
α → γ: Type
γ)
| g: β → γ
g, f: α → β
f, a: α
a => g: β → γ
g (f: α → β
f a: α
a)
Self-test: Using compose', define a function, is_even_len : String → Bool, that takes a string and returns true if it's of even length and false otherwise.
-- Answer here. defis_even_lenis_even_len: ?m.46075-- Be sure you can solve it #eval"Love" -- Expect true #eval"Love": ?m.46079"Love!" -- Expect false #eval"Love!": ?m.46098"Love!!" -- Expect true"Love!!": ?m.46117
Function (Also Known As Lambda) Expressions
Our code for compose' is a little more complex than it needs to be. If all we're going to do is use it to return functions, then (1) we don't expect to have to give a third argument, a; and (2) we expect the type of the return value to be a function type. Here's our final definition of compose. It introduces a new idea: that of anonymous function expressions.
def compose: {α β γ : Type} → (β → γ) → (α → β) → α → γ
compose {α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type} :
(β: Type
β → γ: Type
γ) →
(α: Type
α → β: Type
β) →
(α: Type
α → γ: Type
γ)
| g: β → γ
g, f: α → β
f => (fun a: α
a => g: β → γ
g (f: α → β
f a: α
a))
Note that we've again written the type of compose to emphasize that it takes two functions and returns a function. We then match on the first two function arguments, calling them g and f. Finally, what we return is the value of a new kind of expression: (fun a => g (f a)). It specifies an unnamed function, taking an argument, a, and returning the value of g (f a).
Such a function expression, often called a lambda expression, is defined in Lean by the keyword, fun (you can also use a Greek lower case lambda, λ), then arguments, then =>, then the expression that defines the return value. It's essential to understand that such an expression defines a function: one that is waiting for an argument a and that computes a final result only then.
You can use a function expression anywhere you need a function value. Here are some examples.
-- Here we give new names to old function friends deffun_double := fun (fun_double: Nat → Natn :n: NatNat) =>Nat: Type2 *2: Natn defn: Natfun_square := fun (fun_square: Nat → Natn :n: NatNat) =>Nat: Typen ^n: Nat2 -- Here we pass an unnamed function to composee2: Natcompose (fun (compose: {α β γ : Type} → (β → γ) → (α → β) → α → γn :n: NatNat) =>Nat: Typen%n: Nat2 ==2: Nat0)0: NatString.lengthString.length: String → Nat"Love!""Love!": String
In Lean4, the compose function is Function.compose and the infix notation, ∘, is a convenient way to apply it.
-- Composing functions with Lean infix notation for compose defdouble_after_square := (double_after_square: Nat → Natdouble ∘double: Nat → Natsquare) defsquare: Nat → Natis_even_len'' := (is_even_len'': String → Boolis_even ∘is_even: Nat → BoolString.length)String.length: String → Natdouble_after_squaredouble_after_square: Nat → Nat5 -- expect 505: NatString.lengthString.length: String → Nat"Hello Higher Mathematics!" -- 25"Hello Higher Mathematics!": Stringis_even_len''is_even_len'': String → Bool"Hello Higher Mathematics!" -- false"Hello Higher Mathematics!": String
Welcome to Higher Mathematics
We now have the first really big idea in this course. In elementary mathematics you've always had numbers and ways to combine numbers into new numbers: using operators such as + and *. That is, you've always had an algebra with numbers as basic objects and + and * as operators that you can use to combine them. Now you have a higher algebra with functions as basic objects and composition (∘) an an operator, akin to addition or multiplication, to combine functions into new functions.
Being able to understand, define, and apply the general concept of function composition is a milestone in learning. You will use the concepts embedded in this chapter for the rest of the semester. You should take some time to savor the beautifully concise and powerful concept developed here. Here it is again, expressed even more cleanly.
def compose {α β γ : Type} (g : β → γ) (f : α → β) := λ a => g (f a)
This definition gives names to all of the arguments (before the colon); leaves the return type implicit, which Lean infers; and uses λ instead of fun (for the "fun" of it) to write the function that this compose function is to return.
Extra, extra!
Did you know that Java and Python support lambda expressions? In this section, we'll show you, and present implementations of our apply2 and compose functions in Python. You will now see how to program with higher-order functions in Python. You will also know what we mean when we say that you can expect to be able to do so in many other capable languages, as well. You can run the following code in the VSCode container for this class.
# Here's an ordinary definition of a squaring function
def square(x) : return(x**2)
print(square(5)) # expect 25
# Here's square defined with a Python lambda expression
square = lambda x : x**2
print(square(5)) # Expect: 25
# Here we apply an unnamed lambda to 6; expect 36
print((lambda x : x**2)(6))
# Here's apply2 in Python, where f is a function argument
def apply2(f) :
return lambda x : f(f(x))
# Here we use apply2 to apply a cubing function twice to 2
print(apply2(lambda x: x**3)(2)) # Expect (2^3)^3 = 512
# Here is a general compose in Python; g and f are functions
def compose(g,f) :
return (lambda a : g(f(a)))
# Here's an example of its use; understand this code
cube_after_square = compose((lambda x : x**3),(lambda x : x**2))
print(square_after_cube(3)) # Expect (3^2)^3 = 729
The programming and reasoning principles you learn in Discrete Math and Theory will prove exceptionally valuable to you no matter what languages you are ultimately asked to use for everyday programming.
Data Types: Enumerated and Product Types
In this class so far, we've used a few data types that Lean provides for us, namely Bool, Nat, and String. We will often want to reason about or compute with values of other types. How do we define new data types in Lean? This chapter will teach you how to define new types and how to define functions that take arguments and return results of your own data types.
Chapter Overview
This chapter will cover three broad classes of data types. They are called enumerated types, product types and sum types.
Enumerated Types
An enumerated type has a (typically small) number of constant values. By constant we mean that the values of a type are produced by what we call constructors that don't take any arguments.
The Bool type is an example. Its constructors are called true and false. They take no arguments, and so are, in fact, the two (and the only) values of the Bool type.
Product Types
Whereas the Bool type has two constant constructors, a product type has just one constructor, but one that takes two arguments. You can think of the values of a product type as representing an ordered pair of values, such as (0, true), for example.
Here you'll learn how to define a polymorphic product type that lets you specify (or that Lean infers as) arbitrary types, α and β, for the first and second value in any such pair.
Crucially you will also learn how you can define your functions that use such ordered pair values. To use a pair value, you will generally need to get at the individual (first and second) values inside a pair. Given a pair, p = (f,s), we'll define two elimination functions, fst and snd, where fst p returns f and snd p returns s.
Enumerated Types
Suppose we want to represent the objects in the child's game, Rock Paper Scissors. Let's think of these objects as being of a single type, let's call it object. There are three objects of this type. We can enumerate them. They are rock, paper, and scissors. A type with a finite set of objects is called an enumerated type.
We'll define this game-relaeted type as an example. With this and another data type in hand, we'll then specify the rules of the rock paper scissors game.
We'll begin by enclosing our definitions in a namespace, RPS, short for rock paper scissors. All names within this namespace are prefixed implicitly by RPS, avoiding possible naming conflicts with other names already defined by Lean.
namespace RPS
-- Here's the definition of our RPS object type
inductive Object: Type
Object : Type: Type 1
Type
| rock: Object
rock
| paper: Object
paper
| scissors: Object
scissors
Let's look at the elements of this definition:
- The inductive keyword indicates that we're going to define the set of objects of a new type by giving rules for how those objects can be constructed.
- The name we're giving to our new type is object.
- The colon followed by Type specifies that the type of object is Type. It's a type.
- Finally we have three "construction rules," or constructors for this type. Each constructor in this example just a name and no arguments. In general, constructors can have arguments, as we'll see. With this definition, there are only three values of our object type: rock, paper, and scissors. Check it out.
-- To avoid having to write object.rock (etc) we open the namespace open Objectrock -- type is objectrock: Objectpaper -- type is objectpaper: Objectscissors -- type is objectscissors: ObjectObject -- By golly it's a Type!Object: Type
Now that we have this new data type, we can think about defining functions that operate on values of this type. To illustrate this point, let's define a function that takes two objects and returns of a game where the first is played against the second. There are three possible outcomes in this game--win, lose, and tie--so we can't use Bool, with only two values. We need another type. Let's call it result.
inductive Result: Type
Result : Type: Type 1
Type
| wins: Result
wins
| loses: Result
loses
| ties: Result
ties
open Result
Cool, now we can write the function we want. Let's call it play. It will take two objects and return a result that indicates the outcome when the first object is played against the second. For example, the first rule, below, specifies that scissors ties when it's played against scissors, while scissors wins when it's played against paper.
defplay :play: Object → Object → ResultObject →Object: TypeObject →Object: TypeResult |Result: Typescissors,scissors: Objectscissors =>scissors: Objectties |ties: Resultscissors,scissors: Objectpaper =>paper: Objectwins |wins: Resultscissors,scissors: Objectrock =>rock: Objectloses |loses: Resultpaper,paper: Objectscissors =>scissors: Objectloses |loses: Resultpaper,paper: Objectpaper =>paper: Objectties |ties: Resultpaper,paper: Objectrock =>rock: Objectwins |wins: Resultrock,rock: Objectscissors =>scissors: Objectwins |wins: Resultrock,rock: Objectpaper =>paper: Objectloses |loses: Resultrock,rock: Objectrock =>rock: Objecttiesties: Resultplayplay: Object → Object → Resultscissorsscissors: Objectrock end RPSrock: Object
Polymorphic Types
It's often the case that we want a data type that can contain objects of some arbitrary other type. In Java, for example, a HashList can contain objects of other types. You can have a HashList of Strings, for example.
Example: Box α
A simple example is a polymorphic type, perhaps called Box, each value of which holds some value of some other type. We can have a Box that can hold a Nat, for example, a Box that can hold a string, and a Box that can hold a Bool. The type of object that a Box can hold is part of its type. They're all Boxes, but each one is specialized to hold an object of a specific type.
Compared to our simple enumerated type, we'll need to use two new (to us) capabilities:
- Be able to specify the type of object a Box can hold
- Define a constructor that takes an object of that type
Data Type
Here's a definition that will work for us.
inductive Box: Type → Type
Box (α: Type
α : Type: Type 1
Type) : Type: Type 1
Type
| put: {α : Type} → α → Box α
put (a: α
a : α: Type
α)
Let's explain it.
- inductive is a keyword, as explained above
- Box is the name of our now polymorphic data type
- α is a type argument: the type of object that can fit a given Box
- Given (a : α), (Box a) is a type: a type of Box that can fit α's
- put is the sole constructor, taking an argument (a : α)
- a value of type Box α is simply an application term, (put a) Let's see how to use this definition to put some things in boxes
Constructor
Let's look at how to construct terms of type Box α, where α is a type, such as Nat or String.
open Box -- so we can write put instead of Box.put -- If α is a type, then Box α is a typeBoxBox: Type → TypeNat -- A put application term is a value of this typeNat: Type(putput: {α : Type} → α → Box α1) -- this term is a value of type Box Nat1: Nat(putput: {α : Type} → α → Box α1) -- constructors don't compute/do anything -- We can assign values of our type to to variables def1: Natbox_containing_zero :box_containing_zero: Box NatBoxBox: Type → TypeNat :=Nat: Typeputput: {α : Type} → α → Box α0 -- Box is a type builder, taking arguments of different types def0: Natbox_containing_hello :box_containing_hello: Box StringBoxBox: Type → TypeString :=String: Typeputput: {α : Type} → α → Box α"Hello" -- Lean can usually infer the type of a constructor term def"Hello": Stringbox_containing_hello' :=box_containing_hello': Box Stringputput: {α : Type} → α → Box α"Hello" -- What is the type of Box? It takes a type and yields a type"Hello": String(Box) -- Type → Type (study and understand this) -- A constructor of a polyorphic type is itself polymorphicBox: Type → Type(@Box.put) -- {α : Type} → α → Box α. Understand it!Box.put: {α : Type} → α → Box α
It's important to understand that (put 1) is a term of type Box Nat, (put true) is a term of type Box Bool, and (put "Hello!") is a term of type Box String. Constructors are like functions in that you can form application terms, but these terms don't compute anything. Rather, constructor application terms are the values of any given type.
It's also important to understand that the constructors of a polymorphic type are themselves polymorphic. They implicitly take type arguments. The put constructor for example takes both an implicit type argument, α, and an explicit value of that type. In this sense, they behave like the polymorphic functions we've already seen. Go back and review the work we did on a polymorphic identity function to remind yourself of the details. Then double check your understanding of the type of the put constructor. Note that it takes a type argument, but implicitly, and inferred from the following arguments.
(@put) -- @put : {α : Type} → α → Box αput: {α : Type} → α → Box α
Eliminator
Having put some value in a Box, we will often want to get it back out. To do this, we need to eliminate the box to get at what's inside. The way we eliminate an object to get at what's inside it is by pattern matching! We define a (polymorphic) function, let's call it get, that takes an object of type (Box α) for some type, α.
There is only one possible form for such a value. It must be a term, (put a), where a is the argument provided when the term/value was constructed. So what we're going to do is to use pattern matching to (a) determine which constructor was used to construct the box (in this example there's only one) (2) give a name to the value that was provided to the constructor when the term was constructed. Then we return that now named value from "inside" the box.
def get: {α : Type} → Box α → α
get {α: Type
α : Type: Type 1
Type} : Box: Type → Type
Box α: Type
α → α: Type
α
| put: {α : Type} → α → Box α
put o: α
o => o: α
o
Let's analyze that. The function name is get. It's polymorphic with implicit type argument α. It takes a value of type Box α and from that argument it derives and returns a value of type α. The way it does this is by pattern matching on the argument of type Box α. There's only one way that such an argument can exist: it must have been constructed by the put constructor applied to some object, a, of type α. That is, the value must look like put a. By pattern matching we give the value, a the name, o. That's the key! Now we have a name for the object, a, inside the box, all that's left is to return it. Study this example deeply and be sure you fully understand what's going on.
Here are examples to show it in operation. Remind yourself of the definitions of boxed_nat, etc., from above, as needed to see that the results are as expected.
get (get: {α : Type} → Box α → αputput: {α : Type} → α → Box α1) -- o matches with 111: Natget (get: {α : Type} → Box α → αputput: {α : Type} → α → Box αtrue) -- o matches with truetrue: Boolget (get: {α : Type} → Box α → αputput: {α : Type} → α → Box α"Hello!") -- o matches with "Hello!""Hello!": String
The (Polymorphic) Product Type
From basic algebra you should recall the concept of an ordered pair. For example, in the Cartesian plane, we can identify a point as an ordered pair of real numbers. The pair, (0.5, 1.0), for instance, specifies the point 1/2 unit to the right of the origin and 1 unit up. We now want you to think of ordered pair of real numbers as a type, with (0.5, 1.0) as one of many values of this type.
Now think of an ordered pair as a new kind of box that has two objects inside it. Let's call this new kind of box a Prod box. Prod is short for product, as in the product of two numbers. The ordered pair, (0.5, 1.0) would then be a value of a Prod box holding two real numbers.
Of course there's no reason not to generalize the concept to contents of different types. So we could have a Prod box capable of holding two Nat values, two Strings, or two Bools. For example.
Moreover, there's no reason not to allow ordered pairs of different types of values. For example, considered ordered pairs of String and Nat values. The pair ("Love", 4) is an example.
Indeed, given any arbitrary types, α and β, we can define a type of ordered pairs whose first values are of type α and whose second values are of type β. We now have the idea of the polymorphic product type. It's a type builder with two type arguments. It still has a single constructor, here called pair that takes two arguments, (a : α) and (b : β). The term (Prod.pair a b) then represents the ordered pair, (a, b) : Prod α β. Lean provides the notation (a, b) for any such pair.
namespace cs2120
inductive Prod: Type → Type → Type
Prod (α: Type
α β: Type
β : Type: Type 1
Type) : Type: Type 1
Type
| pair: {α β : Type} → α → β → Prod α β
pair (a: α
a : α: Type
α) (b: β
b : β: Type
β)
open Prod
Constructor
Our pair constructor is polymorphic with two implicit type arguments, α and β, and two explicit arguments, (a : α) and (b : β). The types are inferred. The term, (pair a b) is then of type Prod α β. Take some timeto internalize this structure.
-- Here's the type of the constructor including implicit arguments(@pair) -- Here we build two ordered pair values defpair: {α β : Type} → α → β → Prod α βa_pair_string_nat :a_pair_string_nat: Prod String NatProdProd: Type → Type → TypeStringString: TypeNat :=Nat: Typepairpair: {α β : Type} → α → β → Prod α β"Love""Love": String4 def4: Nata_pair_nat_bool :a_pair_nat_bool: Prod Nat BoolProdProd: Type → Type → TypeNatNat: TypeBool :=Bool: Typepairpair: {α β : Type} → α → β → Prod α β55: Natfalse -- These objects are of the "parameterized" types you expectfalse: Boola_pair_string_nat -- type of ("Love",5) is (Prod String Nat)a_pair_string_nat: Prod String Nata_pair_nat_bool -- type of (5, false) is (Prod Nat Bool)a_pair_nat_bool: Prod Nat Bool
Eliminators
Now suppose we have an ordered pair, p = (a, b), and that we want to get the first, or respectovely the second, value, "out of the box." We'll need two eliminators: one that when given a pair, (a, b), returns the first element, a; and one that returns the second element, b. We take just the same approach as before, using pattern matching to give names to the element inside a given pair. We can then return the right one.
def first: {α β : Type} → Prod α β → α
first {α: Type
α β: Type
β : Type: Type 1
Type} : Prod: Type → Type → Type
Prod α: Type
α β: Type
β → α: Type
α
| (pair: {α β : Type} → α → β → Prod α β
pair a: α
a _) => a: α
a
def second: {α β : Type} → Prod α β → β
second {α: Type
α β: Type
β : Type: Type 1
Type} : Prod: Type → Type → Type
Prod α: Type
α β: Type
β → β: Type
β
| (pair: {α β : Type} → α → β → Prod α β
pair _ b: β
b) => b: β
b
Study, compare, and contrast the function types here as well as the implementations. These rules say, first, if we have both an a and a b, then we can get an a; and, second, if we have both an a and a b, we can get a b. They're really simple, but they're also now stated in a mathematically precise and general way. This is mathematics as much as it is programming. It's abstract mathematics that also actually computes. Here are a few examples of computing with these definitions.
firstfirst: {α β : Type} → Prod α β → αa_pair_string_nat -- "Love"a_pair_string_nat: Prod String Natsecondsecond: {α β : Type} → Prod α β → βa_pair_string_nat -- 4a_pair_string_nat: Prod String Natfirstfirst: {α β : Type} → Prod α β → αa_pair_nat_bool -- 5a_pair_nat_bool: Prod Nat Boolsecondsecond: {α β : Type} → Prod α β → βa_pair_nat_bool -- falsea_pair_nat_bool: Prod Nat Boolfirst (first: {α β : Type} → Prod α β → αpairpair: {α β : Type} → α → β → Prod α β22: Nat"Hi") -- 2"Hi": Stringsecond (second: {α β : Type} → Prod α β → βpairpair: {α β : Type} → α → β → Prod α β22: Nat"Hi") -- "Hi""Hi": String
Challenge: What is the type of Prod? Think hard about it before answering? Hint: It's not Type.
end cs2120
Lean's Built-In Prod Type
Lean provide the type builder, Prod. It's just like what we've defined except that it works not just with types in Type, but with types in any "Type universe," from Type 0 to Type 1 all the way on up. Don't worry about type universe levels at this point.
The details of the definition are a little different, but in ways that aren't important here. The things to know are: (1) given (a : α) and (b : β) the term, (a, b) represents that ordered pair, and is of type Prod α β; (2) the two destructors are called fst and snd; and (3) there is a notation for the type, Prod α β, namely α × β.
-- The name of the single Prod constructor is mk defpair0 :=pair0: Bool × NatProd.mkProd.mk: {α β : Type} → α → β → α × βtruetrue: Bool1010: Natpair0 -- Bool × Nat -- Use standard notation instead of Prod.mk defpair0: Bool × Natpair1 := (pair1: String × Nat"Hello","Hello": String5) def5: Natpair2 := (pair2: Nat × Bool17,17: Natfalse)false: Boolpair1 -- Type is String × Natpair1: String × Natpair2 -- Type is Nat × Bool -- Be sure you understand these function types -- The u_1 and u_2 generalize to Type, Type 1, ...pair2: Nat × Bool(@Prod.fst)Prod.fst: {α : Type u_1} → {β : Type u_2} → α × β → α(@Prod.snd) -- How to eliminate to get the first or second elementProd.snd: {α : Type u_1} → {β : Type u_2} → α × β → βProd.fstProd.fst: {α β : Type} → α × β → αpair1 -- expect "Hello"pair1: String × NatProd.sndProd.snd: {α β : Type} → α × β → βpair1 -- expect 5pair1: String × NatProd.fstProd.fst: {α β : Type} → α × β → αpair2 -- expect 17pair2: Nat × BoolProd.sndProd.snd: {α β : Type} → α × β → βpair2 -- expect false -- We call these "projection" functions -- Lean provides notations for "projection"pair2: Nat × Boolpair1.pair1: String × Nat1 -- expect "Hello"1: {α β : Type} → α × β → αpair1.pair1: String × Nat2 -- expect 52: {α β : Type} → α × β → βpair2.pair2: Nat × Bool1 -- expect 171: {α β : Type} → α × β → αpair2.pair2: Nat × Bool2 -- expect false2: {α β : Type} → α × β → β
An object of an ordered pair of type α × β contains both an object, a : α, AND and object (b : β). Do you see a way to define a new polymorphic type that contains either a value (a : α) OR a (b : β)?. The word OR here means exclusive or.
Data Types: Sum, Unit, and Empty Types
Whereas a product type contains both a value of some type, α, and a value of some type β, a sum type contains either a value of some type, α, or a value of some type, β. A sum type thus has two constructors, each taking a single argument, one taking an α value, the other taking a β value. We'll use asd constructor names inl and inr, where inl takes an argument of type α and inr takes an argument of type β. So, if (a : α), then inl a will be an object of a sum type; and if (b : β) then inr b will also be a value of a sum type.
The bulk of this chapter will deal with sum types, but then we'll address two very simple types, one with a single constant constructor, and one with no constuctors, and thus no values, at all. We will call these the unit and empty types.
Brief Review
Last time we saw defined polymorphic types that we called Box α and Prod α β, where α and β are type parameters. Here are their types.
namespace cs2120 inductiveBox (Box: Type → Typeα :α: TypeType) :Type: Type 1Type |Type: Type 1put (put: {α : Type} → α → Box αa :a: αα)α: Type(@Box.put) defBox.put: {α : Type} → α → Box αfoo := (foo: Box StringBox.putBox.put: {α : Type} → α → Box α"Hello")"Hello": Stringfoofoo: Box String
Here we've renamed the constructor from pair to mk to be consistent with Lean's built-in definition of the Prod type builder.
inductive Prod: Type → Type → Type
Prod (α: Type
α : Type: Type 1
Type) (β: Type
β : Type: Type 1
Type)
| mk: {α β : Type} → α → β → Prod α β
mk (a: α
a : α: Type
α) (b: β
b : β: Type
β)
Let's focus on the Box α type. It has one constructor, put (a : α). This constructor takes an implicit type argument, α, because Box is polymorphic, as well as an explicit argument value of type α. We can see the full type of put using @.
(@Box.put) defBox.put: {α : Type} → α → Box αjack_in_a_box := @jack_in_a_box: Box StringBox.putBox.put: {α : Type} → α → Box αStringString: Type"Jack!""Jack!": String
Leaving implicit arguments enabled, we can leave out the explicit type argument.
def jack_in_a_box': Box String
jack_in_a_box' := Box.put: {α : Type} → α → Box α
Box.put "Jack!": String
"Jack!"
It's important to understand that the constructor, put, doesn't compute anything: it just "wraps" its arguments into a term, here, Box.put "Jack!". You can visualize this as a box, with the label Box.put, and the contents "Jack!". The term Box.put "Jack!" is a value of type Box String.
Finally, we saw that we can get the (string) value from inside a term by eliminating the surrounding structure, giving a name to the string it contains, and returning the string value by that name. The key idea is that this is done by pattern matching.
Take the term, *Box.put "Jack!", as an example, if we match this term with the pattern, "Box.put s", then, (1) it matches, (2) the name s is bound to the string, "Jack!", and we can return that string by returning s. We'll write a get function to do this, and we might as well make it polymorphic.
defget {get: {α : Type} → Box α → αα :α: TypeType}:Type: Type 1BoxBox: Type → Typeα →α: Typeα | (α: TypeBox.putBox.put: {α : Type} → α → Box αs) =>s: αss: αget (get: {α : Type} → Box α → αBox.putBox.put: {α : Type} → α → Box α"Jack!") def"Jack!": Stringsquare (square: Nat → Natn :n: NatNat) :Nat: TypeNat :=Nat: Typen*n: Natnn: Nat
The Prod type builder is analogous except it puts two values, of possibly two different types,into a box, and so we need two "elimination functions" to get those values, called fst and snd in Lean. In Lean the constructor is called Prod.mk, but it's best to use ordered pair notation for that.
end cs2120(ProdProd: Type → Type → TypeNatNat: TypeBool) -- a typeBool: Type(Prod.mkProd.mk: {α β : Type} → α → β → α × β33: Nattrue) -- a value (term)true: Bool(3,3: Nattrue) -- outfix notation -- aka *projection functions*true: BoolProd.fst (Prod.fst: {α β : Type} → α × β → α3,3: Nattrue)true: BoolProd.snd (Prod.snd: {α β : Type} → α × β → β3,3: Nattrue)true: Bool(3,3: Nattrue).true: Bool1 -- postfix notation1: {α β : Type} → α × β → α(3,3: Nattrue).true: Bool2 -- postfix notation2: {α β : Type} → α × β → β
Sum Types
We can call such a type a sum type. We will again give a slightly simplified definition and then explain how to use the concept with Lean's build-in definitions. Here are the key ideas:
- Sum will be polymorphic with two type arguments
- It will have two constructors
- The first (inl) takes (a : α) to construct a value with an α value
- The second (inr) take (b : β) to construct a value with a β value
- To use a value of a sum type we have to be able to handle either case
namespace cs2120
inductive Sum: Type → Type → Type
Sum (α: Type
α β: Type
β : Type: Type 1
Type) : Type: Type 1
Type
| inl: {α β : Type} → α → Sum α β
inl (a: α
a : α: Type
α)
| inr: {α β : Type} → β → Sum α β
inr (b: β
b : β: Type
β)
Constructors
def a_sum1: Sum Nat Bool
a_sum1 : Sum: Type → Type → Type
Sum Nat: Type
Nat Bool: Type
Bool := Sum.inl: {α β : Type} → α → Sum α β
Sum.inl 1: Nat
1
def b_sum1: Sum Nat Bool
b_sum1 : Sum: Type → Type → Type
Sum Nat: Type
Nat Bool: Type
Bool := Sum.inr: {α β : Type} → β → Sum α β
Sum.inr true: Bool
true
These definitions assign (1) to a_sum1 a Sum object capable of holding a Nat OR a Bool, and that contains the Nat value, 1; and (2) to b_sum1, the same type of object but now holding the Bool value, true.
By contrast, the following definition assigns to a_sum2 an object capable of holding a Nat or a String, and holding the Nat value, 1. The value, 1, is the same as in the earlier example, but it's held in a different type of object: one of type Sum Nat String rather than of type Sum Nat Bool.
def a_sum2: Sum Nat String
a_sum2 : Sum: Type → Type → Type
Sum Nat: Type
Nat String: Type
String := Sum.inl: {α β : Type} → α → Sum α β
Sum.inl 1: Nat
1
Eliminator
A value of type Prod α β always contains both an α AND a β value, so given an object of this type we can always return an α value and we can always return a β value. The fst and snd functions serve these purposes.
By contrast, if all we're given an arbitrary value of type Sum α β, while we can be assured that it contains a value of type α OR a value of type β, but we can't be assured that we'll always have a value of type α to return or a value of type β. So we aren't able to define elimination functions like those for Prod α β.
To make good use of an arbitrary value of type Sum α β we need to have a little more machinery lying around. In particular, suppose we have two functions, one to convert any value of type α into, a String (or more generally into any type γ), and that we also have a funtion to convert any value of type β into a String (or more generally a value of that same type γ). The key is is that when given any value of type Sum α β, we can return a String (or more generally a value of some type γ) in either case.
Here's a concrete example.
def elim_sum1: Sum Nat Bool → String
elim_sum1 : Sum: Type → Type → Type
Sum Nat: Type
Nat Bool: Type
Bool → String: Type
String
| (Sum.inl: {α β : Type} → α → Sum α β
Sum.inl _) => "It's a Nat": String
"It's a Nat"
| (Sum.inr: {α β : Type} → β → Sum α β
Sum.inr _) => "It's a Bool": String
"It's a Bool"
We can make this elimination function more general by passing in and using two functions, one that converts any Nat to a String and one that converts and Bool to a string. Here's what that looks like.
def elim_sum2: Sum Nat Bool → (Nat → String) → (Bool → String) → String
elim_sum2 :
(Sum: Type → Type → Type
Sum Nat: Type
Nat Bool: Type
Bool) →
(Nat: Type
Nat → String: Type
String) →
(Bool: Type
Bool → String: Type
String) →
String: Type
String
| (Sum.inl: {α β : Type} → α → Sum α β
Sum.inl n: Nat
n), n2s: Nat → String
n2s, _ => n2s: Nat → String
n2s n: Nat
n
| (Sum.inr: {α β : Type} → β → Sum α β
Sum.inr b: Bool
b), _, b2s: Bool → String
b2s => b2s: Bool → String
b2s b: Bool
b
Let's analyze that. It takes arguments as expected, including Nat-to-String and Bool-to-String conversion functions. It then uses pattern matching to match the two possible forms of the given (Sum Nat Bool) value. If it was constructed using inl with a Nat, then it applies the Nat to String converter to the Nat to get the String to return.
Let's see it in action. We'll define two very simple functions to convert Nats and Bools to strings: each will take an argument and just return the same string we used in the example above.
defnat_to_string (nat_to_string: Nat → String:Nat) :=Nat: Type"It's a Nat" -- argument unused def"It's a Nat": Stringbool_to_string (bool_to_string: Bool → String:Bool) :=Bool: Type"It's a Bool""It's a Bool": String
Now we can apply the elimination function we defined.
elim_sum2elim_sum2: Sum Nat Bool → (Nat → String) → (Bool → String) → Stringa_sum1a_sum1: Sum Nat Boolnat_to_stringnat_to_string: Nat → Stringbool_to_stringbool_to_string: Bool → Stringelim_sum2elim_sum2: Sum Nat Bool → (Nat → String) → (Bool → String) → Stringb_sum1b_sum1: Sum Nat Boolnat_to_stringnat_to_string: Nat → Stringbool_to_stringbool_to_string: Bool → String
We're now in a position to define a general-purpose elimination function for Sum type values. Given three arbitrary types, α, β, and γ, it will take a value, s, of type (Sum α β), a function α2γ : α → γ, and a function, β2γ : β → γ, and will return a value of type γ. The function doesn't can't know ahead of time whether a given s will contain an α or a β value, but it can handle either case.
defelim_sum {elim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γαα: Typeββ: Typeγ :γ: TypeType} : (Type: Type 1SumSum: Type → Type → Typeαα: Typeβ) → (β: Typeα →α: Typeγ) → (γ: Typeβ →β: Typeγ) →γ: Typeγ | (γ: TypeSum.inlSum.inl: {α β : Type} → α → Sum α βa),a: αα2γ, _ =>α2γ: α → γα2γα2γ: α → γa | (a: αSum.inrSum.inr: {α β : Type} → β → Sum α βb), _,b: ββ2γ =>β2γ: β → γβ2γβ2γ: β → γbb: βelim_sumelim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γa_sum1a_sum1: Sum Nat Boolnat_to_stringnat_to_string: Nat → Stringbool_to_stringbool_to_string: Bool → Stringelim_sumelim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γb_sum1b_sum1: Sum Nat Boolnat_to_stringnat_to_string: Nat → Stringbool_to_stringbool_to_string: Bool → String
Sum Types in Everyday Programming
Understanding what it takes, and how, to deal with objects of sum types is another big achievement in this class. It will make you a better programmer, and it's deeply related to logic, and in particular to reasoning from proofs of OR propositions.
Take programming. First, classes in Java and Python are basically product types: an object of a given type has values for all of the fields defined by it class. These languages simply don't have sum types. You can fake them, but it's complicated. Think about it. How would you define a Java class whose objects have either a cat field or a dog field? You can't.
On the other hand, industrial languages such as Rust and Swift, as well as functional languages such as Haskell and OCaml, do support sum types directly. You now have the basic pattern for programming with sum-type values: you have to have a way to handle each case.
end cs2120
The Sum Type in Lean
Given any two types, α and β, you can form the type, Sum α β, with notation α ⊕ β. You create values of this type using the Sum.inl and Sum.inr constructors. Note that if all you give to, say, inl, is a value of type α, Lean won't be able to infer the missing type β. You will have to give an explicit sum type to the value you're defining.
defss: Nat ⊕ ?m.26264-- don't know how to synthesize implicit argument def s1 : Sum Nat Bool := Sum.inl 1 def s2 : Sum Nat Bool := Sum.inr trues1s1: Nat ⊕ Bools2 defs2: Nat ⊕ Boolwhich :which: Nat ⊕ Bool → StringSumSum: Type → Type → TypeNatNat: TypeBool →Bool: TypeString | (String: TypeSum.inl _) =>Sum.inl: {α : Type ?u.26339} → {β : Type ?u.26338} → α → α ⊕ β"Left" | ("Left": StringSum.inr _) =>Sum.inr: {α : Type ?u.26360} → {β : Type ?u.26359} → β → α ⊕ β"Right""Right": Stringwhichwhich: Nat ⊕ Bool → Strings1s1: Nat ⊕ Boolwhichwhich: Nat ⊕ Bool → Strings2s2: Nat ⊕ Bool
Unit Type
The type, Bool, defines a set of two possible values. A variable of this type carries one bit of information, and thus distinguishes between two possibiities.
What about a type with just one value? We can certainly define such a type, and we'll call it the Unit type.
namespace cs2120
We'll present an only slightly simplified version of Lean's Unit type here. This will be all you'll need to use the built-in type.
The type definition is exactly what you'd expect. Unit is a type with one constant (parameterless) constructor, unit. Thus unit is the only value of the Unit type.
inductive Unit: Type
Unit : Type: Type 1
Type
| unit: Unit
unit
open Unit
The Lean libraries define () as a notation for unit. We can do the same with our own types, by the way.
notation "()" =>unitunit: Unit()(): Unit
So how much information does a value of this type carry? Imagine a function that takes some parameter and returns a value of this type. Here's one. It takes a Nat value and returns a Unit value.
defuseless (useless: Nat → Unit:Nat) :Nat: TypeUnit :=Unit: Type()(): Unituselessuseless: Nat → Unit00: Nat
How much do you learn about n from the return value of this operation? How much information does it give you? The answer is, nothing at all. You can of course also pass a value of the Unit type to a function, but it gives the function no useful additional information and so you might just as well leave it out.
def silly: Unit → Nat
silly : Unit: Type
Unit → Nat: Type
Nat
| () => 5: Nat
5
This silly function can't use the value of its argument to decide even between two possible return values, so it only has one possible course of action, here it returns 5. In pratice you'd never write code like this because it's unnecessarily complex and without harm simplifies to just dropping the argument and "returning" the 5.
def silly': Nat
silly' := 5: Nat
5
Now you might think that Unit is a type you've never seen before, but it practice it's omnipresent in code written in such languages as C, C++, Java, etc. It's the type of value returned by a function that "doesn't return anything useful." You know it as void.
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Here, the main method returns void. The function really doesn't return nothing, it returns a value but one that's useless, and so can be ignored (a compiler can thus optimize it away in compiled code).
What you see in this example is that this type is used in cases where a procedure does something useful that does not include returning a useful result. Here the useful action is printing a message on the console! We call such actions side effects.
Lean4, like other useful functional languages such as Haskell, is capable of expressing operations that have side effects, such as sending output to the console. Here's Hello World in Lean4.
end cs2120 defmain :main: IO UnitIOIO: Type → TypeUnit :=Unit: TypeIO.printlnIO.println: {α : Type} → [inst : ToString α] → α → IO Unit"Hello, World!" -- Lean can run this code for us using #eval"Hello, World!": Stringmainmain: IO Unit
The procedure name is main. It "returns" a result of (built-in) type, IO Unit. IO is a polymorphic monadic type. This code basically says "run the side-effecting println routine in an isolated monad that returns Unit (nothing) when it's done."
You can actually write this LEAN code in a file, e.g., HelloWorld.lean, and compile it like a C++ or Java program, then run it, just as you would a compiled Java program.
So now you understand the Unit type in Lean. It's a data type with just one value. It communicates no information, and is useful mainly as a return value of an operation that computes nothing but rather is useful for its side effects, here input/output.
-- Here's Lean's version
Unit.unitUnit.unit: Unit
Empty Type
Just as there's a type, Unit, with just one value, we can define a type, we'll call it empty, with no values at all. It sounds useless. We won't find it useful in programming but it plays a vital role in constructive logic. For now we'll see what we can learn by programming with it.
namespace cs2120
inductive Empty: Type
Empty
That it: no constructors, no values. The Empty type.
inductive empty: Type
empty
What kinds of functions can we write with arguments or return values of the Empty type? Let's look at three possibilities:
- A function that takes Nat and returns Empty
- A function that takes Empty and returns Nat
- A function that takes Empty and returns Empty
defnat2empty :nat2empty: Nat → EmptyNat →Nat: TypeEmpty |Empty: Typen =>n: Nat
There's no way to construct a value of type Empty, because there are no such values, so we can't finish this definition. There are values of type Nat, so we can call this function, but it cannot finish because there's no way to write a return result term of type Empty.
If you try to call it using #reduce, it'll tell you that the function is defined using "sorry", which is to say that the definition is incomplete. (Yes, the error message is confusing. Sorry about that.)
nat2emptynat2empty: Nat → Empty5 -- sorry (doesn't properly reduce)5: Nat
Now let's write a function that takes an argument of type Empty and returns a result of some other type: we might as well just use Nat as an example.
def empty2nat: Empty → Nat
empty2nat : Empty: Type
Empty → Nat: Type
Nat
| e: Empty
e => nomatch e: Empty
e
There's something very odd about this function. It type basically says, "if you give me (e : Empty) I can give you a Nat." Suppose, then you do give such an e. The implementation has to give an result (of type Nat) for each possible case for e. How many cases are there? Zero! So you don't have to give an answer at all! That's the meaning of nomatch e. You don't have to specify an actual natural number result for even one case. The implementation is of the specified type nonetheless. Weird but true and it really makes sense if you think hard about it.
-- You can never call it, so it doesn't matter! defx := (x: Natempty2natempty2nat: Empty → Nat) -- can't give a value
As another example, we can even define a function defined to return a value of type Empty provided it gets on as an argment.
defempty2empty :empty2empty: Empty → EmptyEmpty →Empty: TypeEmpty |Empty: Typee => nomatche: Emptye defe: Emptyx' := (x': Emptyempty2emptyempty2empty: Empty → Empty) -- we can never call it
Indeed, there's nothing special about Nat or Empty as return types in these examples. We can write a function defined to return a value of any type, given a value of the Empty type as an argument. Again, the reason is that such a function to to return a value for each possible constructor/form of e, but there are no constructors/forms, so there are no cases to consider. We can thus define a generalize polymorphic function defined to return a value of any arbitrary type, α, if it's given an argument of the Empty type.
def empty2anytype: {α : Type} → Empty → α
empty2anytype : {α: Type
α : Type: Type 1
Type} → Empty: Type
Empty → α: Type
α
| _, e: Empty
e => nomatch e: Empty
e
end cs2120
Summary So Far
It's worth taking stock of the key ideas you've now learned in this class. We started with the notions of elementary types, such as Bool, Nat, and String, and of values of such types. Now we've seen that if we're given any two types, α and β, we can always form new types, in several ways. In particular, we can form function types, α → β; product types, α × β; and sum types, α ⊕ β.
Function types
Given any two type, α and β, we can form the function type, α → β. The → operator can be understood as taking two types and returning a new type, α → β. Here's a function showing the idea: it takes types α and β and returns a new type, namely the function type, α → β.
-- This is a function that returns a *type* deffunction_type (function_type: Type → Type → Typeαα: Typeβ :β: TypeType) :Type: Type 1Type :=Type: Type 1α →α: Typeββ: Type(function_typefunction_type: Type → Type → TypeNatNat: TypeBool)Bool: Type(function_typefunction_type: Type → Type → TypeNatNat: TypeBool)Bool: Type
A value of a function type is a function implementation that defines a procedure that, if it's given (applied to) a value of type α, then it constructs and returns a value of type β.
def negate: Bool → Bool
negate : Bool: Type
Bool → Bool: Type
Bool
| false: Bool
false => true: Bool
true
| true: Bool
true => false: Bool
false
Here's the same function with a little bit of new syntax. The syntax above is shorthand for this notation. The new element here is a match statement.
-- Learn this new syntax please (match expression) defnegate' :negate': Bool → BoolBool →Bool: TypeBool := -- type funBool: Typex :x: BoolBool => -- assume given Bool x matchBool: Typex with -- case analysis on x |x: Booltrue =>true: Boolfalse -- result in case true |false: Boolfalse =>false: Booltrue -- result in case false -- A *fun* term expresses a function *implementation*true: Bool(funx :x: BoolBool => matchBool: Typex with |x: Booltrue =>true: Boolfalse |false: Boolfalse =>false: Booltrue) -- Sometimes *fun* is written as Greek lambda *λ*true: Bool(λx :x: BoolBool => matchBool: Typex with |x: Booltrue =>true: Boolfalse |false: Boolfalse =>false: Booltrue)true: Bool
So does any of this matter to you if you're a data scientist or ML engineering programming everything in Python? Let's take a little diversion over into Python to see. Can you express anonymous function values values in Python, too? Open lecture_07.py.
Ok, so now we're back in Lean, in which every function is strongly and statically typed. Given any two types, α and β, we can construct the type, α → β; and then to construct a value of type α → β, one must produce a procedure that, if it's given any value of type α, then returns some value of type β.
This is exactly the meaning of a function type, α → β. Note that it's a conditional. It starts with a hypothesis: an assumption. A value of a function type assumes it's given a value of the specified type, and then having made that assumption, it needs to construct and return a value of the specified type. It's for exactly this reason that we can even define a function that takes an argument of a type that has no arguments, and that returns a result of a type that has no values. To wit:
def empty2empty: Empty → Empty
empty2empty : Empty: Type
Empty → Empty: Type
Empty := λ e: Empty
e => e: Empty
e
This example shows that function types are similar to logical implication statements, of the form if a then b. A value of a function type (an implementation) proves the truth of the implication, if you can give me a value of the argument type, then I can return you a value of the result type, even if you'll never be able to provide an argument in the first place.
Exercise: Which rule (case) for determining the truth of an implication in propositional (Boolean) logic is most analogous to the function type, Empty → Empty? Is such a statement true? In a sense, the existence of a function implementation shows the "truth" of such an expression! If you can define an a implementation of this type, that would prove that Empty → Empty.
Exercise: Give a function type involving the Empty type that can't be proved. What is the corresponding rule for evaluating implications in Boolean/propositional logic?
Product types
Given any types, α and β, we can form the product type, Prod α β, written as α × β in conventional mathematical notation.
{αα: Typeβ :β: TypeType} → (Type: Type 1a :a: αα) → (α: Typeb :b: ββ) →β: Typeα ×α: Typeββ: Type
Given a value a : α, and a value, b : β, we can form a value, (a, b) of type α × β, shorthand for Prod.mk a b. This constructor application term, as is, represents the ordered pair, (a, b). It's best to use this conventional mathematical notation.
("Hello","Hello": String5) -- value of type String × Nat5: Nat
To use a value of this type you apply one of the two elimination functions. One "projects" the first element of a pair, and one the second element. These functions are thus also called projection functions in ordinary mathematical discourse.
{αα: Typeβ :β: TypeType} →Type: Type 1α ×α: Typeβ →β: Typeα -- α × β is Prod α βα: Type{αα: Typeβ :β: TypeType} →Type: Type 1α ×α: Typeβ →β: Typeβ -- Sum construction and eliminationβ: Type{αα: Typeβ :β: TypeType} →Type: Type 1α →α: Typeα ⊕α: Typeβ -- α ⊕ β is Sum α ββ: Type{αα: Typeβ :β: TypeType} →Type: Type 1β →β: Typeα ⊕α: Typeββ: Type{αα: Typeββ: Typeγ :γ: TypeType} →Type: Type 1α ⊕α: Typeβ → (β: Typeα →α: Typeγ) → (γ: Typeβ →β: Typeγ) →γ: Typeγ -- Unit constructionγ: TypeUnit.unit -- There is no useful way to use a value of this type -- There is no constructor for Empty -- Empty eliminationUnit.unit: Unit{α :α: TypeType} →Type: Type 1Empty →Empty: Typeα -- Function compositionα: Type{αα: Typeββ: Typeγ :γ: TypeType} → (Type: Type 1β →β: Typeγ) → (γ: Typeα →α: Typeβ) → (β: Typeα →α: Typeγ)γ: Type
Note: This lecture started with an in depth review of the answer key for Homework #3. That took upwards of 40 minutes, then we only had enough time to cover the Unit type.
Data Types: The Unit Type
In this lecture we explore the simplest of all data types. Whereas the Bool type has two values, the Unit type has only one--defined by the single constant constructor, unit. The Empty type has no values, and no constructors, at all. It is said to be an uninhabited type. This chapter presents the Unit type and its related constructors and patterns of usage.
To best understand the Unit type, start with the familiar type, Bool. It has a set of two possible values, namely true and false. A variable of this type carries one of these two values, and thus carries one bit of information, just enough to distinguish between two possible worlds.
So what about a type with just one value? Indeed we can define such a type, and it's usually called the Unit type. We'll present a slightly simplified version of Lean's Unit type here. This will be all you'll need to use the built-in Unit type for now.
The type definition is just what you'd expect. Unit is a type with one constant (parameterless) constructor, unit. Thus unit is the only value of the Unit type.
namespace cs2120
inductive Unit: Type
Unit : Type: Type 1
Type
| unit: Unit
unit
open Unit
The Lean libraries define () as a notation for unit. We can do the same with our own types, by the way. Here's how you can define a notational shorthand for Unit.unit.
notation "()" =>Unit.unitUnit.unit: Unit()(): Unit
So how much information does a value of this type carry? Imagine a function that takes some parameter and returns a value of this type. How much can you learn about the argument by looking at a return value of type Unit? The answer is that it doesn't tell you anything at all. A value of this type carries no information: zero bits. Such a value is devoid of information.
As an example, here's a function that takes any Nat as an argument and that always returns (unit : Unit). You always get the same answer no matter what value of type Nat you give as an argument. The return value tells you nothing (other than that the function ran).
defuseless :useless: Nat → UnitNat →Nat: TypeUnit := funUnit: Type_ =>_: Nat()(): Unituselessuseless: Nat → Unit0 -- returns (), void0: Nat
You can of course also pass a value of the Unit type to a function, but it gives the function no useful additional information to work with, so you might as well leave it out.
def silly: Unit → Nat
silly : Unit: Type
Unit → Nat: Type
Nat
| () => 5: Nat
5
This silly function can't use the value of its argument to decide even between two possible return values, so it only has one possible course of action, here it returns 5. In pratice you'd never write code like this because it's unnecessarily complex. Without harm you can remove the argument from the definition and just return the 5.
def silly': Nat
silly' := 5: Nat
5
Now you might think that Unit is a type you've never seen before, but it practice it's omnipresent in code written in such languages as C, C++, Java, etc. It's the type of value returned by a function that "doesn't return anything useful." You know it as void.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Here, the main method returns void. The function really doesn't return nothing, it returns a value but one that's useless, and so can be ignored (a compiler can thus optimize it away in compiled code).
What you see in this example is that this type is used in cases where a procedure does something useful that does not include returning a useful result. Here the useful action is printing a message on the console! We call such actions side effects.
Lean4, like other useful functional languages such as Haskell, is capable of expressing operations that have side effects, such as sending output to the console. Here's Hello World in Lean4.
end cs2120 defmain :main: IO UnitIOIO: Type → TypeUnit :=Unit: TypeIO.printlnIO.println: {α : Type} → [inst : ToString α] → α → IO Unit"Hello, World!" -- Lean can run this code for us using #eval"Hello, World!": Stringmainmain: IO Unit
The procedure name is main. It "returns" a result of (built-in) type, IO Unit. IO is a polymorphic monad type that (a) runs a computation that, in general, isn't purely functional (such as sending output to the terminal), and (b) returns a result of some type. In this example, that type is Unit.
You can actually write this LEAN code in a file, e.g., HelloWorld.lean, and compile and run it just like a Java or C++ program. Try it in the terminal.
lean --run lecture_08_hello.lean
So now you understand the Unit type in Lean. It's a data type with just one value. It communicates no information, and is useful mainly as a return value of an operation that computes nothing of interest but is instead useful for its side effects.
-- Here's Lean's version of the unit value of the Unit type.
Unit.unitUnit.unit: Unit
The Unit Type in Python
Python has a Unit type, called NoneType, with a single value, None. This value is returned by functions that don't have explicit return values, and can be used explicitly to reflect the absence of a value.
# This file introduces the NoneType type and its single value, None
# None is a (and is the only) value of class NoneType
print(type(None)) # expect NoneType
# Functions without explicit returns implicitly return None
def return_nothing (x : str) :
print(x) # no return statement here
print(return_nothing("Hi!")) # expect "Hi!" then None
# You can use None to represent an error value
# For example, here's a natural number predecessor function
# that returns None to signal error if its argument is <= 0
def nat_pred (n : int) :
if (n <= 0) :
return None
else :
return (n - 1)
print(nat_pred(2)) # expect 1
print(nat_pred(1)) # expect 0
print(nat_pred(0)) # expect None (undefined)
Data Types: The Empty Type
Just as we've defined the Bool type with two values, and the Unit type with just one value, so we can also define a type, we'll call it Empty, with no values at all. In this chapter we'll understand this Empty type by investigating what kinds of functions we can define involving (non-existent) values this type.
namespace cs2120
Definition of the Empty Type
Here's how the type is defined in Lean.
inductive Empty: Type
Empty : Type: Type 1
Type
That's it: no constructors, no values. Voila, the Empty type.
Functions Involving the Empty Type
In the remainder of this chapter we explore whether or not we can implement certain function types involving Empty as either an argument or return type. You're not likely to run into such examples in everyday programming, but understanding these example will be deeply important as we turn to logical reasoning.
No Introduction (Value Creation) Operation for Empty
The Empty type has no introduction operations. It has not even a single constructor so it's impossible to create even a single value of this type. Such a type is said to be uninhabited. A type that has at least one value is said to be inhabited. The Empty type is an uninhabited type.
A consequence of having no constructors and thus no values is that here's no way to complete the binding of a variable of the Empty type. There's no way to complete the following definition.
defe :e: EmptyEmpty :=Empty: Type-- can't express a term of type Empty
No Functions from Inhabited Types to Empty
Nor is there a way to complete the definition of a function that takes an argument of an inhabited type (and which thus can be applied to a value of that type) and that promises to return a value of type Empty. In the following example, we use Unit as a simple example of an inhabited type.
def inhabited_to_empty : Unit → Empty | unit =>-- can't write a term of the Empty type
If you could complete this function definition, and with Unit being inhabited, then you could apply the function to a value of that type; and that point, you'd be stuck with having to do the impossible: return a value of type Empty. You cannot implement a function of type as Unit → Empty, Bool → Empty, or from any inhabited type to Empty.
There Is a Function from Empty to Empty
The only way to define a function that returns a value of the empty type is to have it assume that it's given a value of this type as a parameter.
def empty_to_empty'' : Empty → Empty
| e => e
def empty_to_empty' (e : Empty) := e -- return type inferred
def empty_to_empty (e : Empty) : Empty := nomatch e
This definition is subtle. Clearly it is possible to define a function that promises to return a value of the Empty type, and does so, assuming you apply it to a value of this type. Indeed, the right way to read the Empty → Empty function type is as saying if you give me a value of type Empty, I'll give you back value of type Empty. On the other hand, nowhere does this function definition promise that there's a value of type Empty it can be applied to. Indeed, it's a function that does exist but that can never be applied. That's how it can exist without creating a contradiction.
def empty_value := empty_to_empty'-- no way to apply it
Case Analysis on an Argument of Empty Type
Another way to understand why it's ok to define a function of type Empty → Empty is by considering case analysis on the argument. If the argument were of type Bool, a function definition would have to provide results for both true and false argument values. If the argument were of type Unit, the function definition would have to provide a result for the unit value. But if the argument is of type Empty, the function needs to provide results for no argument values at all! There are no cases to consider. With an assumed argument of type Empty, with no cases to consider, one need do nothing at all to uphold the promise to return a value of any type whatsoever.
Here's how to write our empty-to-empty function in Lean using case analysis. It's with a new keyword that indicates an empty match. Consider matching on Bool, Unit, and then Empty values.
-- First consider case analysis on a Bool argument
def match_bool (b : Bool) : Bool :=
match b with
| true => true
| false => false
-- Now case analysis on a Unit argument
def match_unit (u : Unit) : Unit :=
match u with
| unit => unit
-- Finally case analysis on an Empty argument -- no cases
def empty2nat (e : Empty) : Empty :=
nomatch e -- with no cases to consider, we're done
A Function from Empty to Any Type Whatsoever
Indeed, there's nothing special about the Empty return type in the preceding example. The same trick--matching on Empty requires no further work--works no matter the return type. We can thus implement a function from Empty to any type whatsoever!
def empty_to_bool : Empty → Bool := nomatch e
def empty_to_nat : Empty → Nat := nomatch e
def empty_to_α (α : Type) : Empty → α := nomatch e
The final example is the general elimination rule for the Empty type: an empty match is a get out of jail free card that let's you return a value of any type, even of a type, such as Empty, that has no values at all. There's no contradiction as such a function can never be called, so one need not give an explicit return value.
The Generalized Empty Elimination Operation
We now simply rename the function to empty_elim, to emphasize it's general nature. It shows that if a function assumes it's given a value of type Empty, then it can promise to return a value of any type whatsoever.
def empty_elim (α : Type) : Empty → α := nomatch e
Logically speaking, one can
say that from a contradiction (there is a value
of type Empty), you can deduce anything at all.
It's logically true: if I'm a cat (contradiction)
then gerbils are really tiny neckless giraffes.
What Does a Function of Type (α → Empty) Imply?
As a final key idea, suppose you have some type, α, and you actually can implement a function of type α → Empty. What indisputible and important fact can you conclude about the type, α? What's the only way you will be able to implement such a function?
-- You answer here with a brief explanation
Exercises
- Can you define some function, nxe2s : Nat × Empty → String
- Is the type, Nat × Empty → String, inhabited or not?
- How many strings can nxe2s possibly return? Why?
- Can you define a function, noe2s : String ⊕ Empty → Nat
- Is the type, String ⊕ Empty → Nat, inhabited or not?
- Can noe2s return any Nat? If so, prove it by example.
- Is the function type, (Nat → Empty), inhabited or not?
- Prove your answer (is (Nat → Empty) uninhabited)
- Is the type, {α : Type} → α → Empty, inhabited or not.
- Prove your answer (Is {α : Type} → α → Empty, inhabited?)
- Is the type, Empty → (Nat → Empty) inhabited? Prove it.
- Prove this type uninhabited: {α : Type} → α × (α → Empty)
end cs2120Data Types: Recursive Types
You've seen that we use the keyword, inductive, to introduce new data types, but what does this word really mean?
A Toy Example
To get to an answer, let's have a look at a traditional child's toy: the nesting doll.
Recursive Data
Such a doll comes in two forms: either it's a solid (very inner) doll, or an outer shell with another doll inside it. In the second case, the doll inside can in turn be either a solid doll or another shell with another doll inside it. And that doll can be either a solid doll or a shell with another doll inside. This kind of nesting can go on for any finite number of shells. We can represent the structure of such a doll as an inductive data type. We'll call it doll.
inductive Doll: Type
Doll : Type: Type 1
Type
| solid: Doll
solid
| shell: Doll → Doll
shell (d: Doll
d : Doll: Type
Doll)
open Doll
We can now construct dolls of arbitrary depth, always starting with a solid doll and then iterating the application of shell as many times as desired. It's important to understand that there's no way to apply the shell constructor without first having started with a solid value. On the other hand, once you start with the solid doll, in principle you can apply the shell constructor as many (finite number of) times as you care to to build ever larger dolls.
def d0: Doll
d0 := solid: Doll
solid
def d1: Doll
d1 := shell: Doll → Doll
shell d0: Doll
d0
def d2: Doll
d2 := shell: Doll → Doll
shell d1: Doll
d1
def d3: Doll
d3 := shell: Doll → Doll
shell d2: Doll
d2
We could have written these definitions out showing each application of shell, all the way down to the solid doll.
def d0': Doll
d0' := solid: Doll
solid
def d1': Doll
d1' := shell: Doll → Doll
shell solid: Doll
solid
def d2': Doll
d2' := shell: Doll → Doll
shell (shell: Doll → Doll
shell solid: Doll
solid)
def d3': Doll
d3' := shell: Doll → Doll
shell (shell: Doll → Doll
shell (shell: Doll → Doll
shell solid: Doll
solid))
To drive the point home, we can expand the definition of d3 to see what term it really defines. We'll just keep expanding the "inner" dolls until we reach the final solid doll at the "bottom." That doll has no smaller one as an argument and so can be expanded no further.
- d3 =
- shell d2 =
- shell (shell d1) =
- shell (shell (shell d0)) =
- shell (shell (shell solid))
The final term is the actual value of d3.
The Meaning of Inductive
Now you're seeing the meaning of the term, inductive. It's a central idea in computer science: that we will often want to be able to construct objects of some type, α from smaller objects of the same type, ultimately bottoming out at a least element of that type. We can also say that objects of such a type have a recursive structure.
Exercise
Define a function, inner : Doll → Doll. When applied to any doll, d, if d is the solid doll then inner must return solid, otherwise, if d is a nested doll, then it must be of the form (shell d') for some doll, d', and in this case the function should return d'
definner:inner: Doll → DollDoll →Doll: TypeDoll |Doll: Typesolid =>solid: Dollsolid | (solid: Dollshellshell: Doll → Dolld') =>d': Dolld'd': Dollinnerinner: Doll → Dolld3 -- expect (shell (shell solid))d3: Dollinnerinner: Doll → Dollsolidsolid: Doll
You would be correct to call inner an elimination rule for the Doll type. If a function has to use a Doll given as an argument this is the basic pattern for analyzing it to determine what to do next.
Recursive Functions
Now suppose we're given an arbitrary object of type Doll and that we want to count how many layers deep it goes and return that result as a natural number. For example, a solid doll has zero levels of further nesting, so we'll define its depth to be zero. To compute the depth of an arbitrary doll, we'll function, depth : Doll → Nat. How can we implement it?
Well, given any (d : Doll), we have to analyze d to see which case we're dealing with: solid or shell d' with d' being the doll that figuratively inside the shell. If we find that d is Doll.solid, we return zero. In the other case, if d is of the form (shell d'), then the answer is clearly 1 + depth d'. That is, it's 1 (for the outer shell) plus whatever is the depth of the doll inside the shell. We will call solid the base case and shell d' the recursive case.
def depth: Doll → Nat
depth : Doll: Type
Doll → Nat: Type
Nat
| solid: Doll
solid => 0: Nat
0 -- *base case*
| shell: Doll → Doll
shell d': Doll
d' => 1: Nat
1 + depth: Doll → Nat
depth d': Doll
d' -- recursive case
The depth function is recursive in the sense that it is defined in terms of an application of itself to a smaller value, d', with knowledge that it cannot loop forever. We can easily check to see that it seems to works.
depthdepth: Doll → Natd0d0: Dolldepthdepth: Doll → Natd1d1: Dolldepthdepth: Doll → Natd2d2: Dolldepthdepth: Doll → Natd3d3: Doll
Now, it is worthwhile to convince yourself that the evaluation of a recursve function application really works, so here's how it works when to evaluate depth d3.
- depth (shell (shell (shell solid)))
-
1 + depth (shell (shell solid)) -
1 + 1 + depth (shell solid) -
1 + 1 + 1 + depth solid -
1 + 1 + 1 + 0 -
1 + 1 + 1 -
1 + 2 -
3
Recursive Thinking
Now that you see that it works, it's best to forget about this kind of unrolling of the recursion! The key to really grasping the concept is to understand that when writing you're writing the implementatation of a recusive function, such as depth (shell d'), you can just assume that you already have the value of the answer for the next smaller argument. Here you can assume you already have the value of (depth d'). Once you really grasp this idea, it's easy to define the result: in this case it's just 1 + depth d'.
There Must Be a Least Value
The one thing that must be true is that a recursive function will eventually reach a non-recursive, least, or smallest, "base case" (here solid) after a finite number of steps.
In our doll example, that's assured by the nature of an inductive data definition. By the very definition of an inductive data type, the set of values it defines is the set of values that generated by any finite number of applications of the available constructors. To build a doll you can only start from solid and then apply the shell constructor a finite number of times. When you compute (depth d) by recursion, you apply depth to a one-smaller inner doll, d', and you can only do this as many times as shell was applied to construct d before you read the solid doll, at which point the computation returns a final result. The recursion always terminates after a finite number of steps.
Structural Recursion
We call such recursion "structural recursion." Lean knows that depth is structurally recursive because it can detect that you're calling depth recursively on a proper sub-structure of a given doll. And, as any doll can exist only because of a finite number of applications of the shell constructor to a solid doll, the depth function will eventually reach that solid doll. At that point, the function will return 0 and the overall sum of all the 1's plus that final 0 will be returned.
Lean Verifies Structural Recursions
Indeed, Lean insists that recursions terminate after a finite number of steps. Because structural recursions terminate by definition, Lean doesn't complain as long as it can determine that a recursion is structural. If it can't prove this fact it will reject a definition.
Suppose, for example, that you inadvertently applied depth recursively not to the smaller inner doll, d', but to the given argument, d, instead. In this case, evalating depth d would evaluate depth d and that would evaluate depth d and so on forever. Lean will reject such a definition. Read the error message that Lean produces for the following definition.
def:Doll →Doll: TypeNat |Nat: Typed =>d: Dollbad_depth'bad_depth': Doll → Natd -- not a structural recursiond: Doll
There's a deep reason Lean can't accept such a definition. Suppose it could. Then we could write the following slight variant.
def:Doll →Doll: TypeEmpty -- Lean rejects def |Empty: Typed =>d: Dollbad_depthbad_depth: Doll → Emptydd: Doll(bad_depthbad_depth: Doll → Emptyd3) -- Term of type Empty!!!d3: Doll(bad_depthbad_depth: Doll → Emptyd3) -- Function won't reduced3: Doll
If Lean accepted this definition, then the application term (bad_depth d) would be of type Empty, but there can be no such term! We'd have a logical contradiction and our logical universe would implode; false would be true; and sensible reasoning would become impossible.
The Nat Data Type
While the Doll datatype might seem somewhat irrelevant except as a simple example, it's not. In fact, but for the names we've used (Doll, solid, and shell), it is identical to the definition of the Nat type. That is, natural numbers are represented by nested terms, with Nat.zero as the least value, and Nat.succ (n' Nat) as the way to construct the next larger Nat value from a given one.
-- Our own version of Lean's Nat data type
namespace cs2120 -- The definition of the Nat type
inductive Nat: Type
Nat : Type: Type 1
Type
| zero: Nat
zero
| succ: Nat → Nat
succ (n': Nat
n' : Nat: Type
Nat)
We will and Lean does represent mathematical natural numbers as terms of type Nat in the obvious way. For example, we will take Nat.zero to represent the abstract number we write as 0, (Nat.succ Nat.zero) to represent 1, (Nat.succ (Nat.succ zero)) for 2, and so forth.
Exercise: Define n0, n1, n2, and n3 to be the values of type Nat that we'll take to represent the natural numbers 0, 1, 2, and 3, respectively.
Lean does us a favor by having standard Arabic numeral notations for natural numbers built in. So as long as there's no ambiguity, Lean will interpret 0 to mean Nat.zero, and will output Nat.zero as 0, etc.
-- Switch back to using Lean's definition of Nat
end cs2120
Note: You can use certain Nat notations, such as 0 for Nat.zero and n' + 1 for Nat.succ n' when pattern matching. An annoying detail is that 1 + n' won't work in place of Nat.succ n'. This is one of the few little Lean notational details that you just have to remember.
defn0 :=n0: NatNat.zero defNat.zero: Natn1 :=n1: NatNat.succNat.succ: Nat → Natn0 defn0: Natn2 :=n2: NatNat.succNat.succ: Nat → Natn1 defn1: Natn3 :=n3: NatNat.succNat.succ: Nat → Natn2 defn2: Natis_zero'' :is_zero'': Nat → BoolNat →Nat: TypeBool -- this works but is verbose |Bool: TypeNat.zero =>Nat.zero: Nattrue | (true: BoolNat.succNat.succ: Nat → Nat) =>false deffalse: Boolis_zero' :is_zero': Nat → BoolNat →Nat: TypeBool |Bool: Type0 =>0: Nattruetrue: Bool-- (1 + n') not a valid pattern def is_Zero' : Nat → Bool -- our preferred notation | 0 => true -- 0 for Nat.zero |+ 1 =>false -- n' + 1 is a valid patternfalse: Bool
Exercises
#1: Write a function, pred: Nat → Nat, that takes an any Nat, n, and, if n is zero, returns zero, otherwise returns Nat that is one smaller than n. We call it the predecessor of n, in contradistinction to the successor of n (n + 1). Hint: Look at how you wrote inner for the Doll type.
defpred :pred: Nat → NatNat →Nat: TypeNat |Nat: Type0 =>0: Nat0 | (0: NatNat.succNat.succ: Nat → Natn') =>n': Natn' -- destructures nat>0 to (succ n') -- Answer here -- test casesn': Natpredpred: Nat → Nat3 -- expect 23: Natpredpred: Nat → Nat0 -- expect 00: Nat
#2: Write a function, mk_doll : Nat → Doll, that takes any natural number argument, n, and that returns a doll n shells deep. The verify using #reduce that (mk_doll 3) returns the same doll as d3.
-- Answer here defmk_doll :mk_doll: Nat → DollNat →Nat: TypeDoll | _ =>Doll: Type__: Doll-- test casesmk_dollmk_doll: Nat → Doll33: Natmk_dollmk_doll: Nat → Doll33: Nat
#3: Write a function, nat_eq : Nat → Nat → Bool, that takes any two natural numbers and that returns Boolean true if they're equal, and false otherwise. Hint: How many cases do you need to consider?
defnat_eq :nat_eq: Nat → Nat → BoolNat →Nat: TypeNat →Nat: TypeBool |Bool: Type0,0: Nat0 =>0: Nattrue |true: Bool0,0: Natn' + 1 =>n': Natfalse |false: Booln' + 1,n': Nat0 =>0: Natfalse | (false: Booln' + 1), (n': Natm' + 1) =>m': Natnat_eqnat_eq: Nat → Nat → Bool
#3: Write a function, nat_le : Nat → Nat → Bool, that takes any two natural numbers and that returns Boolean true if the first value is less than or equal to the second, and false otherwise. Hint: The key to solving this problem is to figure out the relevant cases, match on them, and then return the right result in each case.
-- Here def nat_le : Nat → Nat → Bool | 0, _ => true | (n' + 1), 0 => false | (_), (_) =>-- expect true-- exect true-- expect false-- expect true-- expect false-- expect true
#4: Write a function nat_add : Nat → Nat → Nat, that takes two Nat values and returns the Nat representing their sum. Method: You could do case analysis on either argument, but, to be consistent with Lean's definitions, do case analysis on the second (Nat) argument, returning the right result in either case.
The challenge is to rewrite the inductive case to one employing structural recursion. Remember, you assume you have (1) a successor constructor, succ, and (2) a recursive solution for the sum in the case where the second argument to the recursive function application is structurally smaller than that argument to nat_add.
def add : Nat → Nat → Nat | m, 0 => m | m, (Nat.succ n') =>
Propositional Logic
We've now built enough machinery and intuition for basic logic that not only we can introduce it informally, in the manner you'd find in almost any textbook, but we can also formalize it in Lean. Doing so has several major benefits, including making the ideas precise and concise, while also enabling automated logical reasoning. In this chapter we'll do exactly that.
Formal Languages: Syntax and Semantics
Propositional logic is an example of a formal language with a syntax and a semantics. A formal language is an artificial (as opposed to natural) language that has a mathematical definition. The syntax of such a language specifies its set of well formed expressions (sometimes called well formed formulae, or wffs). The semantics provides a way to assign a meaning to each and every wff.
The language of arithmetic is a formal language. Its wffs include the following:
- *1 + 113
- 10 = 11*
- "x * y = 1"
On the other hand, the following strings of symbols are not in the set of expressions defined by the syntax of arithmetic.
- + x +
- 1 >== 2
- Hello
The semantics of the language of arithmetic in turn give us a way to assign meanings to the well formed expressions.
- In the first example, we interpret + as arithmetic addition, and the overall arithmetic expression then means 113.
- In the second case, we interpret = as the equality relation, giving us a Boolean expression, the meaning of which in this case is false.
- The third expression means either true or false depending on the meanings of the arithmetic variables, x and y.
The third case is the most interesting because it shows that the semantics of arithmentic is not just a simple function from wwfs to meanings. We need an additional piece of information in this case, which we can call a valuation, or interpretation, of the variables. The meaning of x * y = 1 is true under the valuation {x = 2, y = 1/2}, for example, but is false under the valuation {x = 2, y = 1}.
Speaking informally, the semantics of arithmetic is thus a function that takes an expression and also a valuation of the variables that might appear in it and that returns a meaning.
Propositinal Logic Informally
Propositional logic is among the simplest of useful formal languages. You already know some version of it from having learned how to read and write Boolean expresions when Programming in Python, Java, or any other ordinary programming language.
Atomic Propositions
Propositional logic starts with the notion of an atomic proposition. An atomic proposition, P, is a declarative statement that cannot be broken into smaller propositions, and for which it makes sense to ask Is it true or not that P?. Here are some examples of atomic propositions:
- It's raining
- The ground is wet
- x * y = 1
Because atomic propositions don't break up into smaller elements, and to make it easier to read and write expressions, it's the usual practice to use variables, sometimes called propositional letters, to stand for longer propositions. For example, we could define the following shorthands.
- a = "it's raining"
- b = "the ground is wet"
- c = "x * y = 1"
Here are some examples of propositions that are not atomic. Be sure you see that they are made up of smaller propositions.
- If it's raining then the ground is wet
- a implies b (using propositional letters)
- x * y = 1 or x * y ≠ 1 (x and y are not propositional letters in this context, but refer to numbers)
Finally, here are some correct expressions in English that are not propositions at all:
- Is Mary home?
- Get out!
- Please pass the jelly.
None of these expressions pass the simple "Is it true" test for being a proposition:
- Is it true or not that "Is Mary home?" -- makes no sense
- Is it true or not that "Get out!" -- makes no sense
- Is it true or not that "Please pass the jelly." -- makes no sense
Inductively Defined Syntax
The syntax of propositional logic defines the set of well formed formula (expressions), inductively. In other words, we can build larger expressions from smaller ones. Here are the rules.
- If a is an atomic formula then {a} is an expression
- If b and c are expressions, then so are the following:
- ¬b -- not b
- b ∧ c -- b and c
- b ∨ c -- b or c
- b ⇒ c -- b implies c; if b then c
- b ↔ c -- b if and only if c; b and c are equivalent
That's it! Here then are some valid expressions in propositional logic:
- {a} -- it's raining
- {b} -- the ground is wet
-- Now assume that a and b are any expressions
- ¬a -- it's not raining
- a ⇒ b -- if it's raining then the ground is wet
- a ∨ b -- it's raining or the ground is wet
- (a ∧ b) ∨ ¬a -- (raining and wet) or (not raining)
Note: Most informal (English/natural language) definitions of propositional logic don't distinguish between atomic formula (represented by a propositional variables, such as a and b) and expressions that incorporate them ({a} and {b} in our notation). Rather, it's common just to say that if a is an atomic formula then it's also an expression.
We distinguish a as an atomic variable from from {a}, an (atomic) expression. This separation of variables from single-variable expressions will enable us to define valuations as functions, from variables only (not from all expressions) to (true/false) values.
We'll then define the meaning of any expression as a separate function: that take any expression along with a valuation of any variables it might contain and that then returns its true or false meaning. Our formal specification will clarify this distinction.
Semantics
From here on out, we'll write atomic propositions using single-letter variables such as a, b, and c. Now to assign a meaning any possible expression we will have to answer a few questions:
- What does each variables mean?
- What do the connective symbols mean?
- How is the meaning of an expression computed from the meaning of its parts?
Atomic Propositional Variables
First, we will assigning a true or false the meaning to each atomic propositional variable in an expression by way of a valation. For example, if we only care about one such variable, say a, then there are two possible valuations:
- { a ↦ true }
- { a ↦ false }
If two variables, a and b might appear in expressions, then there are four interpretations.
- {a ↦ true, b ↦ true}
- {a ↦ true, b ↦ false}
- {a ↦ false, b ↦ true}
- {a ↦ false, b ↦ false}
You should recognize these lists as the input sides of truth tables, with the output column determined by the expression to be evaluated. Here's an example. See first that the input (left) side lists variables, while the right side lists values of expressions. Second, note that that there are two interpretations for the one input variable, a.
| a | ¬{a} |
|---|---|
| true | false |
| false | true |
The output expression can be more complex, but the number of interpretations is determined only by the number of variables that have to be given values to determine the value of the output expression.
| a | {a} ∨ ¬{a} |
|---|---|
| true | true |
| false | true |
Finally here's an example of a list of all interpretations, and the corresponding values, for an expression employing two propositional variables, a and b. The row give the four possible interpretations of the two variables, on the left, while the right column gives the values of the logical formula (expression) under each of these interpretations.
| a | b | {a} ∨ {b} |
|---|---|---|
| true | true | true |
| true | false | true |
| false | true | true |
| false | false | false |
The main conclusions at this point are as follows. First, a valuation assigns Boolean truth values to propositional variables. Second, a truth table lists each possible valuation for a given set of variables. Third, the output column of a truth table specifies a logical expression and gives its truth value for each corresponding valuation.
A final observation is that a single row of a truth table specifies a function from propositional variables to to Boolean (truth) values. We could for example write the preceding truth table as follows:
| a | b | {a} ∨ {b} |
|---|---|---|
| i₃ | true | |
| i₂ | true | |
| i₁ | true | |
| i₀ | false |
Here the i (for interpretation values are functions from variables to Bool. For example, i₃(a) = true and i₃(b) = true, while i₂(a) = true but i₂(b) = false.
The values on the right (output) side of the truth table are then obtained by first applying the correspond i functions to the variables, a and b, from which the atomic propositional expresssions {a} and {b} are constructed to obtain the meanings of these basic expressions. Then the meaning of ∨ is applied to these two values to obtain the final result. Here, as you'd expect, the meaning of ∨ in propositional logic is the Boolean or function.
You are now ready to use you acquired logic knowledge and skills formalizing concepts in Lean to define the formal language of propositional logic, both syntax and semantics, within the logic of the Lean prover. Indeed, one of the main use cases for Lean is exactly to define domain-specific languages (DSLs). You're now going formally specify your first working DSL!
Propositional Logic Formalized and Automated
Syntax
Variables
We will represent propositional variables as terms of a type called var. Each var object will carry a single natural number as a field value. Different variables will have different natural number indices. You can think of the terms of this type as v₀, v₁, ... ad infinitum. We'll use tick marks on this definition on the way to giving you a better way to write such type definitions.
inductive var': Type
var' : Type: Type 1
Type
| mk: Nat → var'
mk (n: Nat
n : Nat: Type
Nat)
def v₀': var'
v₀' := var'.mk: Nat → var'
var'.mk 0: Nat
0
def v₁': var'
v₁' := var'.mk: Nat → var'
var'.mk 1: Nat
1
def v₂': var'
v₂' := var'.mk: Nat → var'
var'.mk 2: Nat
2
def v₃': var'
v₃' := var'.mk: Nat → var'
var'.mk 3: Nat
3
Here's a new Lean syntactic feature. When you define a datatype with just a single constructor, you can use the structure keyword. You then think of the arguments as fields.
structure var: Type
var : Type: Type 1
Type :=
(n: var → Nat
n: Nat: Type
Nat)
The default constructor name for a structure type is mk. Here we construct four propositional logic variables and give them nice names. There's nothing special about using subscripts in the names. It's just a mathy thing to do, and makes it easier to write subsequent code/logic.
def v₀: var
v₀ := var.mk: Nat → var
var.mk 0: Nat
0
def v₁: var
v₁ := var.mk: Nat → var
var.mk 1: Nat
1
def v₂: var
v₂ := var.mk: Nat → var
var.mk 2: Nat
2
def v₃: var
v₃ := var.mk: Nat → var
var.mk 3: Nat
3
Using the structure feature allows you to use the field names as getter functions, rather than having to write your own using pattern matching (as we did with the fst and snd functions for extracting the elements of a pair). You can use either function application or dot notation.
var.nvar.n: var → Natv₂ -- application notation open var -- not a good ideav₂: varnn: var → Natv₂ -- but it worksv₂: varv₂.v₂: varn -- dot notationn: var → Nat
Connectives (Operators)
inductive unary_op: Type
unary_op : Type: Type 1
Type
| not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
Expressions (Sentences)
inductive Expr: Type
Expr : Type: Type 1
Type
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
open Expr
-- Examples
def s0: Expr
s0 := var_exp: var → Expr
var_exp v₀: var
v₀
def s1: Expr
s1 := var_exp: var → Expr
var_exp v₁: var
v₁
def s2: Expr
s2 := un_exp: unary_op → Expr → Expr
un_exp unary_op.not: unary_op
unary_op.not s0: Expr
s0
def s3: Expr
s3 := un_exp: unary_op → Expr → Expr
un_exp unary_op.not: unary_op
unary_op.not s1: Expr
s1
def s4: Expr
s4 := bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.and: binary_op
binary_op.and s0: Expr
s0 s3: Expr
s3
def s5: Expr
s5 := bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.or: binary_op
binary_op.or s0: Expr
s0 s3: Expr
s3
def s6: Expr
s6 := bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.or: binary_op
binary_op.or s4: Expr
s4 s5: Expr
s5
Notations
Lean supports user-defined notations. The notation system is pretty sophisticated. In general, though, the idea is that you specify how you want to write a term using your own notation, along with how that notation should translate into ordinary Lean terms. See https://lean-lang.org/lean4/doc/notation.html for details.
In what follows, we define standard logical notations for each of the standard connectives, or operators, of propositional logic. We start with one non-standard notation for lifting atomic propositional variables to expressions in the usual language of propositional logic.
After the first, each notation defines the fixity of the corresponding operator, which is to say, where it's placed relative to its arguments; the associativity of the infix operators (all of them are "r" for "right") associative in this case; and the operator precedence, or binding strength, corresponding to the same idea in arithmetic, which states that * applies before +, for example.
notation "{"v: Lean.TSyntax `term
v"}" => var_exp: var → Expr
var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => un_exp: unary_op → Expr → Expr
un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.imp: Type
binary_op.imp
infixr:20 " ⇔ " => bin_exp: binary_op → Expr → Expr → Expr
bin_exp binary_op.iff: Type
binary_op.iff
-- Now we have a "concrete" syntax for our language!
def e0: Expr
e0 := {v₀: var
v₀}
def e1: Expr
e1 := ¬e0: Expr
e0
def e2: Expr
e2 := e0: Expr
e0 ∧ e1: Expr
e1
def e3: Expr
e3 := e0: Expr
e0 ∨ e1: Expr
e1
def e4: Expr
e4 := (e2: Expr
e2 ∧ e3: Expr
e3) ∨ e0: Expr
e0
Semantics
Interpretations/Valuations
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool -- interp is a type
-- examples
def all_true: Interp
all_true : Interp: Type
Interp := fun _: var
_ => true: Bool
true
def all_false: Interp
all_false : Interp: Type
Interp := fun _: var
_ => false: Bool
false
Exercise: Define the i₂ interpretation from above as a function in Lean. It will help to give the name a to v₀ and b to v₁, then define a valuation that gives each of these variables the specified value. It doesn't matter what values you give all the other variables. Just assign them a default value, such as false.
Operators
The meanings of the operators in proposition logic are simply the corresponding Boolean functions. Some of them are unary, taking just one Boolean argument (e.g., not). Others are binary (and as and) and so take two arguments. You can in fact define all kinds of such functions. And _if_then_else operator would take three Boolean arguments, and return a result that uses the first to select on of the following two values as the result.
As above, we'll give semantic meanings to the syntactic connectives by defining functions from the former to the Boolean functions that express their desired meanings.
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
Expressions
And now for the coup de grace: We define a function that gives each and every expression in the language of propositional logic a Boolean meaning. The function is recursive: derive meanings for subexpressions, if any, and then combine them using the right Boolean operators. Atomic expressions are evaluated by interpreting the variables they contain under an interpretation function given to the expression evaluation function as an argument.
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| (var_exp: var → Expr
var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (un_exp: unary_op → Expr → Expr
un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (bin_exp: binary_op → Expr → Expr → Expr
bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Demonstration
eval_expreval_expr: Expr → Interp → Boole0e0: Exprall_trueall_true: Interpeval_expreval_expr: Expr → Interp → Boole1e1: Exprall_trueall_true: Interpeval_expreval_expr: Expr → Interp → Boole2e2: Exprall_trueall_true: Interpeval_expreval_expr: Expr → Interp → Boole3e3: Exprall_trueall_true: Interpeval_expreval_expr: Expr → Interp → Boole4e4: Exprall_trueall_true: Interpeval_expreval_expr: Expr → Interp → Boole0e0: Exprall_falseall_false: Interpeval_expreval_expr: Expr → Interp → Boole1e1: Exprall_falseall_false: Interpeval_expreval_expr: Expr → Interp → Boole2e2: Exprall_falseall_false: Interpeval_expreval_expr: Expr → Interp → Boole3e3: Exprall_falseall_false: Interpeval_expreval_expr: Expr → Interp → Boole4e4: Exprall_falseall_false: Interp
You have now implemented the abstract syntax and standard concrete syntax for, and the semantics of, the formal language of propositional logic. You have also automated the semantic evaluation of variables, operators, and arbitrarily complex expressions in propositional logic. That's cool!
Propositional Logic: Review and Practice
Specification of Propositional Logic
We begin by reproducing our formal specification of the syntax and semantics of propositional logic, without distracting test cases, implementation alternatives, or explanatory text.
Abstract Syntax
structure var: Nat → var
var : Type: Type 1
Type :=
(n: var → Nat
n: Nat: Type
Nat)
inductive unary_op: Type
unary_op : Type: Type 1
Type
| not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
| imp: binary_op
imp
inductive Expr: Type
Expr : Type: Type 1
Type
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
Concrete Syntax
notation "{"v: Lean.TSyntax `term
v"}" => Expr.var_exp: var → Expr
Expr.var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.imp: binary_op
binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.iff: Type
binary_op.iff
Semantics
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def implies: Bool → Bool → Bool
implies : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, false: Bool
false => false: Bool
false
| _, _ => true: Bool
true
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
| binary_op.imp: binary_op
binary_op.imp => implies: Bool → Bool → Bool
implies
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| (Expr.var_exp: var → Expr
Expr.var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Review and Practice
Propositions
A proposition is an expression that asserts that some state of affairs holds in some world, real or imaginary. It makes sense to ask whether a given proposition is true or false in some world.
Here's an example of a proposition: "The red block is on top of the blue block." It makes sense to ask, "Is it true that the red block is on top of the blue block?" However, to answer this question, we also have to specify a world in which we are to evaluate it truth or falsity.
For example, imagine two children, say Bob and Sally, each playing with blocks. We can ask "Is it true that the red block is on top of the blue block in Sally's world?" We can ask "Is it true that the red block is on top of the blue block in Bob's world?" And we may well get different answers. We evaluate the truth of a proposition in a specified world.
Propositional Logic
There are many different logics. Each provides a language of propositions, different kinds of worlds, and formal methods for assessing the truth of a given proposition in a given world.
Propositional logic is an especially simple logic. It provides a language of atomic propositions, a way of building larger propositions by combining smaller ones (using the not (¬), and (∧), or (∨), implies (⇒), and equivalence (↔) connectives, and a recursive function for evaluating the truth of an expression given (a world) a function that assigns Boolean values to each propositional variable that might appear in the proposition.
Variables Represent Atomic Propositions
In propositional logic, one represents an atomic proposition using a variable name. For example, one could represent the atomic proposition, The red block is on top of the blue block using the variable, red_block_on_blue_block. Similarly, one could represent the atomic proposition, The yellow block is on the red block using the rather verbose variable, yellow_block_on_red_block.
Larger Propositions Are Built Using Connectives
We could then write the larger proposition, The red block is on the blue block AND the yellow block is on the red block as red_block_on_blue_block ∧ yellow_block_on_red_block. More generally, we can form larger propositions by applying logical connectives such as ¬, ∧, and ∨, to (the right number of) smaller propositions, bottoming out at atomic propositions.
Abstracting to Short Variable Names
Using long and expressive variable names makes larger propositions hard to write and read. The usual practice, then is to use single character variable names to represent atomic propositions.
Here for example we might just use r to represent the red on blue proposition and y to represent the yellow on red proposition. Now we can write the concise, formal expression, r ∧ y, to stand for the proposition that The red block is on the blue block and the yellow block is on the red block. In practice one could provide an informal translation table linking short variable names to their intended natural language meanings.
| variable | intended meaning |
|---|---|
| r | red block is on blue block |
| y | yellow block is on red block |
Abstracting from Real-World Meanings
The underlying purpose of a logic is to provide a way to express propositions in such a way that we can then reason about their truth or falsity using only the rules of logic, without further reference to their intended informal meanings. We translate natural thoughts into mathematical representations (logic) then use the mathematics to reason further, and finally we can translate logical conclusions back into natural world meanings at the end of the process.
Validity and Unsatisfiability
Furthermore, when studying logic, we are often interested in whether a given proposition in true or false independent of the meanings of its parts. For example, in propositional logic, the proposition, r ∧ ¬r cannot be true no matter what natural language proposition r means: as a proposition cannot be true and false. We call such a proposition unsatisfiable.
Similarly, the proposition, r ∨ ¬r is always true in propositional logic: as a proposition can only be true or false, and in either case one of the two sub-expressions will be true, so the overall one will be true as well. We call such a proposition valid.
For numerous reasons, then, we'll usually use single letters to represent (natural language) propositions, and moreover, we'll often do so without referring to any particular natural language translations. That is, we'll study logic in the abstract. When we show that an abstract proposition is valid, then we can plus in any informal meanings we want for the variables and we still still have logically correct statements.
Consider, for example, the valid abstract proposition, A ∧ B ⇒ A. Now suppose A means "the cat is old" and B means "the dog is a puppy." Then the logical statement means if the cat is old AND the dog is a puppy THEN the cat is old. Valid propositions thus emerge as general principles for logically sound reasoning, no matter what the atomic propositional variables are defined to mean.
HOMEWORK:
Refer to each of the problems in HW5, Part 1. For each one, express the proposition that each function type represents using our formal notation for propositional logic. We'll take you through this exercise in steps.
#1. Propositional Variables
First, define b, c, j, and a as propositional variables (of type var). We'll use b for bread or beta,* c for cheese, j for jam, and a for α*.
defb :=b: varvar.mkvar.mk: Nat → var0 def0: Natj :=j: varvar.mkvar.mk: Nat → var1 def1: Natc :=c: varvar.mkvar.mk: Nat → var2 def2: Nata :=a: varvar.mkvar.mk: Nat → var3 -- get the index out of the c structure3: Natc.c: varnn: var → Nat
#2. Atomic Propositions
Define B, C, J and A as corresponding atomic propositions, of type Expr.
def B: Expr
B := {b: var
b}
def C: Expr
C := {c: var
c}
def J: Expr
J := {j: var
j}
def A: Expr
A := {a: var
a}
#3. Compound Propositions
Now define the variables, e0 through e3, as expressions in propositional logic using the concrete syntax we've defined.
-- #1. ((no jam) ⊕ (no cheese)) → (no (jam × cheese))
def e0: Expr
e0 := (¬J: Expr
J ∨ ¬C: Expr
C) ⇒ ¬(J: Expr
J ∧ C: Expr
C)
-- YOU DO THE REST
#4. Implement Syntax and Semantics for Implies and Biimplication
Next go back and extend our formalism to support the implies connective. Do the same for biimplication while you're at it. This is already done for implies. Your job is to do the same for bi-implication, which Lean does not implement natively.
#5. Evaluate Propositions in Various Worlds
Now evaluate each of these expressions under the all_true and all_false interpretations. These are just two of the possible interpretations so we won't have complete proofs of validity, but at least we expect them to evaluate to true under both the all_true and all_false interpretations.
eval_expreval_expr: Expr → Interp → Boole0 (λe0: Expr_ =>_: varfalse) -- expect truefalse: Booleval_expreval_expr: Expr → Interp → Boole0 (λe0: Expr_ =>_: vartrue) -- expect true -- You do the resttrue: Bool
#6. Evaluate the Expressions Under Some Other Interpretation
Other than these two, evaluate the propositions under your new interpretation, and confirm that they still evaluate to true. Your interpretation should assign various true and false values to j, c, b, and a. An interpretation has to give values to all (infinitely many) variables. You can do case analysis by pattern matching on a few specific variables (by index) then use wildcard matching to handle all remaining cases.
-- Answer herePropositional Logic: A Satisfiability Solver
You will recall that we say that an expression (formula) in propositional logic is valid if it evaluates to true under all possible interpretations (Boolean values for the variables that appear in it). It is satisfiable if there is at least one intepretation under which it is true. And unsatisfiable if there's not even on interpretation that makes the expression true.
We can understand these concepts clearly in terms of truth tables. As we've seen, each of the input columns represents a variable that can appear in the expression being evaluated. Each row up to but no including the final column represents one of the possible intepretations of the variables in the column (a function mapping those variables to Boolean values). The final column lists the values of the expression for each of the intepretations.
As an example, consider the propositional logic expression, X. Here's the truth table for it. There's one input column, for the one variable. There are two interpretations (rows), that we can write as {X ↦ T} and {X ↦ F}. Finally, the output column gives the truth value of the expression we're evaluating, X, under each interpretation.
| v₀ | {v₀} |
|---|---|
| T | T |
| F | F |
From such a truth table it's trivial to determine whether a formula is valid, satisfiable, or unsatisfiable. If all output values are true, the formula is valid. It is also said to be a tautology. If at least one output is true, the formula is satisfiable, and the interpretations that make it true are said to be models of the formula, or solutions. And if all the outputs are false, the expression is unsatisfiable and so has no models/solutions. From the truth table here, we can easily see that X is not valid, but it is satisfiable, with {X ↦ T} as a model.
Exercise: Is the propositional logic expression, X ∧ ¬X valid, satisfiable, or unsatisfiable? What are its models, if any?
| v₀ | {v₀} ∧ ¬{v₀} |
|---|---|
| T | _ |
| F | _ |
Exercise: Answer the same two questions for X ∨ ¬X. To derive your answers, fill in and analyze the truth tables for the two expressions.
| v₀ | {v₀} ∨ ¬{v₀} |
|---|---|
| T | _ |
| F | _ |
Chapter Plan
In the rest of this chapter we'll see how we can perhaps automate the generation of truth tables for any expressions, and thus have what we need to automatically determine its validity, satisfiability, or unsatisfiability, and models if any.
The key element will be a function that, when given the number, v, of variables (columns) in a truth table, computes a list of all 2^v interpretation functions. By evaluating the expression under each interpretation we'll get the list of output values in the truth table. We then just analyze that list to determine whether all, some, or none of the output values is true. The models of the expression, if any, are the interpretations in the rows that have true output values.
The rest of this chapter is in several sections. First we include our specification of the syntax and semantics of propositional logic, without commentary. You can skip over this section unless you need to review the definitions.
Next, we define a function that when given the number of variables, v, in an expression, computes the input side of a truth table as a list of 2^v rows, Each row is a distinct list of v Boolean values, as would appear in a paper-and-pencil truth table, and represents one of the 2^v possible interpretations of the variables that might appear in the expression. The i'th entry in each row is the value assigned by that row/interpretation to the i'th variable (column), with i ranging from 0 to v-1.
Here's an example of the input side of a truth table for an expression with two variables. Be sure you can explain precisely what function, from variables to Boolean values, each row represents.
| v₀ | v₁ | |
|---|---|---|
| i₀ | F | F |
| i₁ | F | T |
| i₂ | T | F |
| i₃ | T | T |
We can write the interpretation functions, corresponding to the rows, as follows.
- i₀ = {v₀ ↦ F, v₁ ↦ F}
- i₁ = {v₀ ↦ F, v₁ ↦ T}
- i₂ = {v₀ ↦ T, v₁ ↦ F}
- i₃ = {v₀ ↦ T, v₁ ↦ T}
Next, the trickiest part, we define a function that takes as its input a row of Boolean values, {b₀, ..., bᵥ₋₁} and that returns the corresponding interpretation function, of type Interp (i.e., var → Bool). We can then use such a function as an argument to our semantic function, eval_expr to compute the truth value of a given expression under that particular interpretation.
Each such interpretation function behaves as follows. For i in the range of 0 to v-1, given the i'th variable, vᵢ (mk_var i) as input, it returns bᵢ, the Boolean value in the i'th position in the row of Boolean values, as its output. Otherwise, for variables with i indices outside the range of interest, it just returns a default value, here false.
Next, we define a function that takes a list of all 2^v rows of a truth table, containing Boolean values, and convert it into a list of interpretation functions, of type Interp.
Finally, from there, we can easily implement the functions that we really want: take any propositional logic expression and tell us if it's valid, satisfiable, or unsatisfiable. We will also want to be able to get a list of models (interpretations) for any given expression.
Propositional Logic Syntax and Semantics
Here we just repeat our specification of the syntax and semantics of propositional logic. By know you are expected to understand the concrete syntax of propositional logic, as defined here, and also the purpose of the eval_expr semantic evaluator, that takes any expression in this syntax along with an interpretation and returns the truth value of the expression under the given interpretation of its atomic propositional variables.
structure var: Nat → var
var : Type: Type 1
Type := (n: var → Nat
n: Nat: Type
Nat)
inductive unary_op: Type
unary_op : Type: Type 1
Type | not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
| imp: binary_op
imp
| iff: binary_op
iff
inductive Expr: Type
Expr : Type: Type 1
Type
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
notation "{"v: Lean.TSyntax `term
v"}" => Expr.var_exp: var → Expr
Expr.var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.imp: binary_op
binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.iff: binary_op
binary_op.iff
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def implies: Bool → Bool → Bool
implies : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, false: Bool
false => false: Bool
false
| _, _ => true: Bool
true
def iff: Bool → Bool → Bool
iff : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, true: Bool
true => true: Bool
true
| false: Bool
false, false: Bool
false => true: Bool
true
| _, _ => false: Bool
false
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
| binary_op.imp: binary_op
binary_op.imp => implies: Bool → Bool → Bool
implies
| binary_op.iff: binary_op
binary_op.iff => iff: Bool → Bool → Bool
iff
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| (Expr.var_exp: var → Expr
Expr.var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Generating Truth Table Rows
Our first function will take as its input the number of variables (columns), v, for which the input side of a truth table is to be generated. We will represent this value as list of 2^v rows, each of which is a list list of v Boolean values. Here, for example, is a table that represents the input side of a truth table with three variables.
| v₀ | v₁ | v₃ | |
|---|---|---|---|
| 0 | F | F | F |
| 1 | F | F | T |
| 2 | F | T | F |
| 3 | F | T | T |
| 4 | T | F | F |
| 5 | T | F | T |
| 6 | T | T | F |
| 7 | T | T | T |
Key Insight
Let's see what pattern emerges if we replace each true and false value in this table with a corresponding binary digit (bit), 0 for false and 1 for true.
| v₀ | v₁ | v₃ | |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 2 | 0 | 1 | 0 |
| 3 | 0 | 1 | 1 |
| 4 | 1 | 0 | 0 |
| 5 | 1 | 0 | 1 |
| 6 | 1 | 1 | 0 |
| 7 | 1 | 1 | 1 |
Do you see it? What's the relationship between the row index and the sequence of binary digits in each row? Think about it before reading on. The answer: each row of binary digits represents the corresponding row index in binary notation. In other words, the rows of a truth table really correspond directly to binary representations of corresponding row indices, ranging from 0 to 2^v - 1. The upshot is that we can compute the r'th row of the input side of a truth table by doing nothing more than representing the row index as a binary numeral, padded with zeros on the left so that it's at least v digits wide, and then replacing the binary digits with the corresponding Boolean truth values.
Algorithm Design
These insights unlock an algorithm design strategy. First, define a function that, when given a number of variables, v, and a row index, r, returns the interpretation that associates the variables in the columns to the Boolean represented by the bit values in the binary representation of r.
Once we have this function, it'll be straightforward to take a number, v, of variables, v, and return a list of the 2^v rows for the input side of a truth table.
Row Index to List of Binary Digits
We'll start by defining a function that converts a given natural number into its binary representation as a list of binary digits. We'll just the natural numbers, 0 and 1 to represent binary digits.
The key observations here are (1) the rightmost bit of a binary representation of a number is 0 if the number is even and 1 if it's odd; (2) the rest of the bits are shifted one place to the right, dropping the rightmost bit, by division by 2. So what we do, while bits remain is to repeatedly (1) compute the rightmost bit, (2) shift the rest of the bits right.
To illustrate, consider row number, 5. The binary representation of 5 is 101. From right to left that's 12^0 + 02^1 + 12^2 = 1 + 0 + 4 = 5. The number is odd so the rightmost bit is 1, which is the remainder, 5 % 2 = 1*. Now recurse on 5 shifted right by one bit, i.e., on 5/2 using natural number division. 5/2 is 2, so the nexxt bit is 2 % 2 = 0. Recursing on 2 / 2 = 1, we find the next bit to be 1 % 2 = 1. Finally recurse on 1 / 2 = 0. Zero is one of the two base cases, so we're done. The result is the sequence of bits [1, 0, 1]. Exercise: Do it yourself for 6. What's the second base case?
Here's code. We abstract the computations of the right bit and shift right operations as named functions. The recursion, sadly, is not structural.
defright_bit (right_bit: Nat → Natn :n: NatNat) :=Nat: Typen%n: Nat2 def2: Natshift_right (shift_right: Nat → Natn :n: NatNat) :=Nat: Typen/n: Nat2 def2: Nat:Nat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Nat1 => [1: Nat1] |1: Natn' + 2 => have : (n': Natshift_right (shift_right: Nat → Natn' +n': Nat2)) < (2: Natn' +n': Nat2) :=2: Natsorrysorry: shift_right (n' + 2) < n' + 2nat_to_bin (nat_to_bin: Nat → List Natshift_right (shift_right: Nat → Natn' +n': Nat2)) ++ [2: Natright_bit (right_bit: Nat → Natn' +n': Nat2)]2: Nat
Okay, you're wondering, what's that weird expression, have : (shift_right (n' + 2)) < (n' + 2) := sorry? To make a long story short, the recursion here is not structural. (Why?) That means that Lean won't be able to prove to itself that the argument to the recursion is always decreasing, which is needed to prove that the recursion always terminates. To avoid Lean giving an error, we have to give Lean an explicit proof that the argument decreases on each recursive call. The mystery code tells Lean that we have such a proof. The sorry says, but we're not going to give it now; just trust us. That's good enough for Lean not to complain. For now, that's what you need to know.
nat_to_binnat_to_bin: Nat → List Nat0 -- expect [0]0: Natnat_to_binnat_to_bin: Nat → List Nat1 -- expect [1]1: Natnat_to_binnat_to_bin: Nat → List Nat3 -- expect [1,1]3: Natnat_to_binnat_to_bin: Nat → List Nat5 -- expect [1,0,1]5: Natnat_to_binnat_to_bin: Nat → List Nat6 -- expect [1,1,0]6: Nat
Left Pad with Zeros
We represent F (false) values in our truth tables as zeros. One remaining issue is that we have to make each output list of binary digits v digits long, to include F values (zeros) on the left. That is, we need to left-pad our lists with zeros so that each list is at least v digits wide.
To do so, we'll iteratively prepend zeros to a given list a number of times equal to v minus the length of a given list. In Lean v - l is zero in all cases where l ≥ v (these are natural numbers), so there is nothing left to do if the input list is long enough.
It's a pretty easy recursion. We take this opportunity to show how you can do a little top-down programming by writing a top-level program that uses one that is then provided as a sort of private subroutine. Learn how to write code like this.
Finally, we note that it's very common to implement a function as a non-recursive top-level program that in turn calls a recursive subroutine, as illustrated here.
defzero_pad :zero_pad: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Typev,v: Natl =>l: List Natzero_pad_recursive (zero_pad_recursive: Nat → List Nat → List Natv - (v: Natl.l: List Natlength))length: {α : Type} → List α → Natl wherel: List Natzero_pad_recursive :zero_pad_recursive: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Type0,0: Natl =>l: List Natl |l: List Natv'+1,v': Natl =>l: List Natzero_pad_recursivezero_pad_recursive: Nat → List Nat → List Natv' (v': Nat0::0: Natl)l: List Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat0]0: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1,1: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat0,0: Nat1,1: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1,1: Nat0,0: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat5 [5: Nat1,1: Nat0,0: Nat1]1: Nat
We can now write a function that will produce the required list of binary digits for the (input part of the) n'th row of a truth table with v variables (columns).
defmk_bit_row : (mk_bit_row: Nat → Nat → List Natrow:row: NatNat) → (Nat: Typecols :cols: NatNat) →Nat: TypeListList: Type → TypeNat |Nat: Typer,r: Natc =>c: Natzero_padzero_pad: Nat → List Nat → List Natc (c: Natnat_to_binnat_to_bin: Nat → List Natr)r: Natmk_bit_rowmk_bit_row: Nat → Nat → List Nat55: Nat6 -- expect [0, 0, 0, 1, 0, 1]6: Nat
List of Bits to List of Bools
Next we need a function to convert a list of bits (Nats) to a list of corresponding Bools. We will convert Nat zero to false, and any other Nat to true.
-- Convert nat to bool where 0 ↦ false, ¬0 ↦ true defbit_to_bool :bit_to_bool: Nat → BoolNat →Nat: TypeBool |Bool: Type0 =>0: Natfalse | _ =>false: Booltruetrue: Boolbit_to_boolbit_to_bool: Nat → Bool0 -- expect false0: Natbit_to_boolbit_to_bool: Nat → Bool1 -- expect true1: Nat
With this element conversion function we can now define our list conversion function. There are two cases. First,' given an empty list of Nat we return an empty list of Bool. Second, we have a bit (Nat) at the head of a non-empty list, in which case we return a list with that Nat converted to a Bool at the head, and the conversion of t, the rest of the list of Nats, into a list of Bools recursively.
defbit_list_to_bool_list :bit_list_to_bool_list: List Nat → List BoolListList: Type → TypeNat →Nat: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Natt => (t: List Natbit_to_boolbit_to_bool: Nat → Boolh) :: (h: Natbit_list_to_bool_listbit_list_to_bool_list: List Nat → List Boolt) -- expect [false, false, false, true, false, true]t: List Natbit_list_to_bool_list [bit_list_to_bool_list: List Nat → List Bool0,0: Nat0,0: Nat0,0: Nat1,1: Nat0,0: Nat1]1: Nat
Make the r'th Row of a Truth Table with v Variables
Now we can easily define a function that when given a truth table row number and the number of variables (columns) Given row and columns return list of Bools
defmk_row_bools : (mk_row_bools: Nat → Nat → List Boolrow :row: NatNat) → (Nat: Typevars :vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: Typer,r: Natv =>v: Natbit_list_to_bool_list (bit_list_to_bool_list: List Nat → List Boolmk_bit_rowmk_bit_row: Nat → Nat → List Natrr: Natv) -- expect [false, false, false, true, false, true]v: Natmk_row_boolsmk_row_bools: Nat → Nat → List Bool55: Nat66: Nat
List Bool → Interp
We now devise an algorithm to convert a list of Booleans, [b₀, ..., bᵥ₋₁] into an interpretation. Denote (mk_var i), the i'th variable, as vᵢ. The idea then is to convert the Boolean list into an interpretation function with the following behavior: { v₀ ↦ b₀, ..., vᵥ₋₁, vᵢ → false for i ≥ v}.
So how do we turn a list of values into a function from variables to Boolean values? In short, we start with some interpretation, given a variable and a value for it, we return a new function that's exactly the same as the given one except when the variable argument is the same as the variable for which we want to return a new value. In that case, the new function returns the new value when it is applied to the variable whose value is being overridden. The top-level algorithm will then iteratively override the value of each variable according to the values in a given row of a truth table.
The hardest-to-understand function is override. Given an interpretation, a variable whose value is to be overridden, and a new value for that variable, we return a function that when given any variable does a case analysis: if the variable is other than the one being override, we just use the given interpretation to compute and return a result, otherwise the new function returns the specified new value.
defoverride :override: Interp → var → Bool → InterpInterp →Interp: Typevar →var: TypeBool →Bool: TypeInterp |Interp: Typeold_interp,old_interp: Interpvar,var: _root_.varnew_val => (λnew_val: Boolv => if (v: _root_.varv.v: _root_.varn ==n: _root_.var → Natvar.var: _root_.varn) -- when applied to var thenn: _root_.var → Natnew_val -- return new value elsenew_val: Boolold_interpold_interp: Interpv) -- else retur old value defv: _root_.varv₀ :=v₀: varvar.mkvar.mk: Nat → var0 def0: Natv₁ :=v₁: varvar.mkvar.mk: Nat → var1 def1: Natv₂ :=v₂: varvar.mkvar.mk: Nat → var2 -- Demonstration def2: Natall_false :all_false: InterpInterp := λInterp: Type_ =>_: varfalsefalse: Boolall_falseall_false: Interpv₀ -- expect falsev₀: varall_falseall_false: Interpv₁ -- expect falsev₁: varall_falseall_false: Interpv₂ -- expect false -- interp for [false, true, false], i.e., [0, 1, 0] defv₂: varinterp2 :=interp2: Interpoverrideoverride: Interp → var → Bool → Interpall_falseall_false: Interpv₁v₁: vartruetrue: Boolinterp2interp2: Interpv₀ -- expect falsev₀: varinterp2interp2: Interpv₁ -- expect truev₁: varinterp2interp2: Interpv₂ -- expect false -- interp for [false, true, true], i.e., [0, 1, 1] defv₂: varinterp3 :=interp3: Interpoverrideoverride: Interp → var → Bool → Interpinterp2interp2: Interpv₂v₂: vartruetrue: Boolinterp3interp3: Interpv₀ -- expect falsev₀: varinterp3interp3: Interpv₁ -- expect truev₁: varinterp3interp3: Interpv₂ -- expect truev₂: var
To turn a list of Booleans into an interpretation, we thus start with some base interpretation, such as all_false, then iterate through the entries in the list, repeatedly overriding the most recently computed interpretation with the so-called maplet, { vᵢ ↦ bᵢ }. The end result will be the interpretation { v₀ ↦ b₀, ..., vᵥ₋₁ ↦ bᵥ₋₁, ...}, with all variables after vᵥ₋₁ being mapped to the value given by the starting interpretation (in practice, all_false, for us).
Note: We introduce a new Lean programming mechanism: the ability to define a function in terms of a "sub-routine" that is subsequently defined in a where block. It is common to define functions this way when the top-level function is non-recursive and takes or computes some additional data that it then passes on to a recursive function that does most of the work.
defbools_to_interp :bools_to_interp: List Bool → InterpListList: Type → TypeBool →Bool: TypeInterp |Interp: Typel =>l: List Boolbools_to_interp_helperbools_to_interp_helper: Nat → List Bool → Interpl.l: List Boollengthlength: {α : Type} → List α → Natl wherel: List Boolbools_to_interp_helper : (bools_to_interp_helper: Nat → List Bool → Interpvars :vars: NatNat) → (Nat: Typevals :vals: List BoolListList: Type → TypeBool) →Bool: TypeInterp | _, [] =>Interp: Typeall_false |all_false: Interpvars,vars: Nath::h: Boolt => lett: List Boollen := (len: Nath::h: Boolt).t: List Boollengthlength: {α : Type} → List α → Natoverride (override: Interp → var → Bool → Interpbools_to_interp_helperbools_to_interp_helper: Nat → List Bool → Interpvarsvars: Natt) (t: List Boolvar.mk (var.mk: Nat → varvars -vars: Natlen))len: Nath -- Demonstration defh: Boolinterp3' :=interp3': Interpbools_to_interp [bools_to_interp: List Bool → Interpfalse,false: Booltrue,true: Booltrue]true: Boolinterp3'interp3': Interpv₀ -- expect falsev₀: varinterp3'interp3': Interpv₁ -- expect truev₁: varinterp3'interp3': Interpv₂ -- expect truev₂: var
From Number of Variables and Row Index to Interpretation
Building in steps, we next define a function that takes a number of variables and a row index and that returns the corresponding interpretation function. It uses mk_row_bools to create the right row of Boolean values then applies the preceding bools_to_interp function to it to return the corresponding interpretation function.
defmk_interp_vars_row : (mk_interp_vars_row: Nat → Nat → Interpvars:vars: NatNat) → (Nat: Typerow:row: NatNat) →Nat: TypeInterp |Interp: Typev,v: Natr =>r: Natbools_to_interp (bools_to_interp: List Bool → Interpmk_row_boolsmk_row_bools: Nat → Nat → List Boolrr: Natv) defv: Natinterp3'' :=interp3'': Interpmk_interp_vars_rowmk_interp_vars_row: Nat → Nat → Interp33: Nat3 -- vars=3, row=3 -- Demonstration3: Natinterp3''interp3'': Interpv₀ -- expect falsev₀: varinterp3''interp3'': Interpv₁ -- expect truev₁: varinterp3''interp3'': Interpv₂ -- expect truev₂: var
From Number of Variables to List of Interpretations
Finally, now, given nothing but a number of variables, we can iteratively generate a list of all 2^v interpretations. We use the same style of function definition above, where the top-level program computes 2^v from v and then passes 2^v (the number of interpretations/rows to generate, along with v, the number of variables, to a recursive function that does most of the work.
def mk_interps: Nat → List Interp
mk_interps (vars: Nat
vars : Nat: Type
Nat) : List: Type → Type
List Interp: Type
Interp :=
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper (2: Nat
2^vars: Nat
vars) vars: Nat
vars
where mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper : (rows: Nat
rows : Nat: Type
Nat) → (vars: Nat
vars : Nat: Type
Nat) → List: Type → Type
List Interp: Type
Interp
| 0: Nat
0, _ => []: List Interp
[]
| (n': Nat
n' + 1), v: Nat
v => (mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row v: Nat
v n': Nat
n')::mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper n': Nat
n' v: Nat
v
(e : Expression) → Nat, The Number of Variables In e
-- Analyze and understand how this function works! defmax_variable_index :max_variable_index: Expr → NatExpr →Expr: TypeNat |Nat: TypeExpr.var_exp (Expr.var_exp: var → Exprvar.mkvar.mk: Nat → vari) =>i: Nati |i: NatExpr.un_exp _Expr.un_exp: unary_op → Expr → Expre =>e: Exprmax_variable_indexmax_variable_index: Expr → Nate |e: ExprExpr.bin_exp _Expr.bin_exp: binary_op → Expr → Expr → Expre1e1: Expre2 =>e2: Exprmax (max: {α : Type} → [self : Max α] → α → α → αmax_variable_indexmax_variable_index: Expr → Nate1) (e1: Exprmax_variable_indexmax_variable_index: Expr → Nate2)e2: Exprmax_variable_index {max_variable_index: Expr → Natv₀}v₀: varmax_variable_index ({max_variable_index: Expr → Natv₀} ∧ {v₀: varv₂}) -- Given expression, return number of variables it assumes defv₂: varnum_vars :num_vars: Expr → NatExpr →Expr: TypeNat := λNat: Typee =>e: Exprmax_variable_indexmax_variable_index: Expr → Nate +e: Expr1 /- Generate list of 8 interpretations for three variables -/ def1: Natinterps3 :=interps3: List Interpmk_interpsmk_interps: Nat → List Interp33: Natinterps3.interps3: List Interplength -- expect 8length: {α : Type} → List α → Nat
From List Interp and Expr to List of Bool Outputs
Now how about a function that takes a list of interpretations and an expresssion and that produces a list of output values?
def eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps : List: Type → Type
List Interp: Type
Interp → Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| [], _ => []: List Bool
[]
--| h::t, e => (eval_expr e h)::eval_expr_interps t e
| h: Interp
h::t: List Interp
t, e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps t: List Interp
t e: Expr
e ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
The change in the preceding algorithm made after class puts the list of output values in order with respect to our enumeration of interpretations.
-- Test/Demonstration caseseval_expr_interps (eval_expr_interps: List Interp → Expr → List Boolmk_interpsmk_interps: Nat → List Interp2) ({2: Natv₀} ∧ {v₀: varv₁}) -- [F,F,F,T]v₁: vareval_expr_interps (eval_expr_interps: List Interp → Expr → List Boolmk_interpsmk_interps: Nat → List Interp2) ({2: Natv₀} ∨ {v₀: varv₁}) -- [F,T,T,T]v₁: var
From Expr to max Variable Index
But our interface isn't yet ideal. We're providing an expression as an argument, and from it we should be able to figure out how many variables are involved. In other words, we shouldn't have to provide a list of interpretations as a separate (and here the first) argument. The observation that leads to a solution is that we can analyze any expression to determine the max index of any variable appearing in it. If we add 1 to that index, we'll have the number of variables in the expression and thus the number of columns in the truth table. We can then use mk_interps with that number as an argument to create the list of interpretations, corresponding to truth table rows, that ne need to pass to eval_expr_interps to get the list of outputs values.
Expr → List Bool: One Value For Each Interpretation
Here's a really important function. Given an expression in propositional logic (using our syntax) it returns the list of outputs values under each of the possible interpretations of the variables (thus atomic expressions) in the given expression.
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps (mk_interps: Nat → List Interp
mk_interps (num_vars: Expr → Nat
num_vars e: Expr
e)) e: Expr
e
Demonstration/Tests: Confirm that actual results are as expected by writing out the truth tables on paper. Note that in the second case, with the max variable index being 2 (Z is var.mk 2), we have 3 variables/columns, thus 8 rows, and thus a list of 8 output values.
Let's give nicer names to three atomic propositions (i.e., variable expressions).
def X: Expr
X := {v₀: var
v₀}
def Y: Expr
Y := {v₁: var
v₁}
def Z: Expr
Z := {v₂: var
v₂}
Now we can produce lists of outputs under all interpretations of variables from index 0 to the max index of any variable appearing in the given expression. Confirm that the results are expected by writing out the truth tables on paper, computing the expected outputs, and checking them against what we compute here.
truth_table_outputs (truth_table_outputs: Expr → List BoolX ∧X: ExprY)Y: Exprtruth_table_outputs (truth_table_outputs: Expr → List BoolX ∨X: ExprZ) -- Write the truth tables on paper then check hereZ: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ∧X: ExprY) ∨ (Y: ExprX ∧X: ExprZ))Z: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ∨X: ExprY) ∧ (Y: ExprX ∨X: ExprZ)) -- Study expression and predict outputs before looking. -- What names would you give to these particular propositions?Z: Exprtruth_table_outputs ((¬(truth_table_outputs: Expr → List BoolX ∧X: ExprY) ⇒ (¬Y: ExprX ∨ ¬X: ExprY)))Y: Exprtruth_table_outputs (((¬truth_table_outputs: Expr → List BoolX ∨ ¬X: ExprY) ⇒ ¬(Y: ExprX ∧X: ExprY)))Y: Exprtruth_table_outputs ((¬(truth_table_outputs: Expr → List BoolX ∨X: ExprY ) ⇒ (¬Y: ExprX ∧ ¬X: ExprY)))Y: Exprtruth_table_outputs (((¬truth_table_outputs: Expr → List BoolX ∧ ¬X: ExprY) ⇒ ¬(Y: ExprX ∨X: ExprY )))Y: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ⇔X: ExprY))Y: Expr
HOMEWORK PART 1:
Write three functions
- sat : Expr → Bool
- unsat: Expr → Bool
- valid: Expr → Bool
Given any expression, e, in propositional logic, the first returns true if e is sastisfiable, otherwise false. The second returns true if e is unsatisfiable, otherwise false. The third returns true if e is valid, and otherwise returns false. You can write helper functions if/as needed. Write short comments to explain what each of your functions does. Write a few test cases to demonstrate your results.
-- Here defreduce_or :reduce_or: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typefalse |false: Boolh::h: Boolt =>t: List Booloror: Bool → Bool → Boolh (h: Boolreduce_orreduce_or: List Bool → Boolt) deft: List Boolreduce_and :reduce_and: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typetrue |true: Boolh::h: Boolt =>t: List Booloror: Bool → Bool → Boolh (h: Boolreduce_andreduce_and: List Bool → Boolt) deft: List Boolis_sat :is_sat: Expr → BoolExpr →Expr: TypeBool := λBool: Typee :e: ExprExpr =>Expr: Typereduce_or (reduce_or: List Bool → Booltruth_table_outputstruth_table_outputs: Expr → List Boole) defe: Expris_valid :is_valid: Expr → BoolExpr →Expr: TypeBool := λBool: Typee :e: ExprExpr =>Expr: Typereduce_and (reduce_and: List Bool → Booltruth_table_outputstruth_table_outputs: Expr → List Boole) -- A few testse: Expris_valid (is_valid: Expr → BoolX) -- expect falseX: Expris_sat (is_sat: Expr → BoolX) -- exect trueX: Expris_sat (is_sat: Expr → BoolX ∧ ¬X: ExprX) -- expect false #evalX: Expr(X ∧ ¬X) -- expect true(X ∧ ¬X): ?m.180690is_valid (is_valid: Expr → BoolX ∨ ¬X: ExprX) -- expect trueX: Expris_valid ((¬(is_valid: Expr → BoolX ∧X: ExprY) ⇔ (¬Y: ExprX ∨ ¬X: ExprY))) -- expect trueY: Expris_valid (¬(is_valid: Expr → BoolX ∨X: ExprY) ⇒ (¬Y: ExprX ∧ ¬X: ExprY)) -- expect trueY: Expr-- expect false -- Test cases
A SAT Solver
A SAT solver takes an expression, e, and returns a value of the sum type, SomeOrNone, an instance of which which holds either some model, if there is at least one, or nothing. We use a sum type, Interp ⊕ Unit: (Sum.inl m) returns the model, m, while Sum.inr Unit.unit signals that there is no model to return.
def SomeModelOrNone := Interp ⊕ Unit -- This is a *type* /- Here's the function. Note thus use of several "let bindings" in this code. They bind names, as shorthands, to given terms, so a final return value can be expressed more succinctly and clearly. This is a common style of coding in most functional programming languages. Here we bind names to two terms, then the expression, *find_model interps e*, defines the return value. -/ def get_model_fun : Expr → SomeModelOrNone | e => let num_vars := num_vars e let interps := (mk_interps num_vars) find_model interps e where find_model : List Interp → Expr → SomeModelOrNone | [], _ => Sum.inr Unit.unit | h::t, e => if (eval_expr e h) then Sum.inl h else find_model t e -- Testsget_model_fun (get_model_fun: Expr → SomeModelOrNoneX) -- expect Sum.inl _ (a function)X: Exprget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprX) -- expect Sum.inr Unit.unit -- List of Booleans for first *num_vars* variables under given Interp defX: Exprinterp_to_bools :interp_to_bools: Interp → Nat → List BoolInterp → (Interp: Typenum_vars :num_vars: NatNat) →Nat: TypeListList: Type → TypeBool | _,Bool: Type0 =>0: Nat[] |[]: List Booli, (i: Interpn' + 1) =>n': Natinterp_to_boolsinterp_to_bools: Interp → Nat → List Boolii: Interpn' ++ [(n': Nati (i: Interpvar.mkvar.mk: Nat → varn'))]n': Nat
Given some model, return list of Boolean values of first num_vars variables, or in the case of no model, just return an empty list.
defsome_model_or_none_to_bools :some_model_or_none_to_bools: SomeModelOrNone → Nat → List BoolSomeModelOrNone → (SomeModelOrNone: Typenum_vars :num_vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: TypeSum.inlSum.inl: {α : Type ?u.194322} → {β : Type ?u.194321} → α → α ⊕ βi,i: Interpn =>n: Natinterp_to_boolsinterp_to_bools: Interp → Nat → List Boolii: Interpn |n: NatSum.inr _, _ =>Sum.inr: {α : Type ?u.194352} → {β : Type ?u.194351} → β → α ⊕ β[] -- Test cases[]: List Boolsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprY))Y: Expr22: Natsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprX))X: Expr22: Natsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (¬get_model_fun: Expr → SomeModelOrNoneX ∨ ¬X: ExprY))Y: Expr2 -- list of all models, then convert to list of lists of bools?2: Nat
Model Finders and Counterexample Generators
The main topic of this chapter is model and counterexample generation: given a proposition in propositional logic, find models if there are any, and similarly find counterexamples if there are any.
We'll begin by generalizing some patterns we've been seeing in functions that handle lists. The first section introduces and illustrates the use of List map, foldr, and filter functions.
Second, we'll see that with these functions in hand and a better understanding of recursion, we can improve our propositional logic satisfiability checking functions.
Finally, we will introduce the concept of a model finder for expressions in propositional logic, also known as a SAT solver, and see how that idea can also provide a way to generate counterexamples to propositions that are not always true.
Higher-Order Functions On Lists
List.map
The List.map function, converts a list of α terms, into a list of corresponding β values by applying a given function, f : α → β, to each α in turn. E.g., map (λ (s : String) => s.length) ["Hello", "Lean"] returns [5, 4].
Here's the type of List.map in the Lean libraries.
@List.mapList.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List βList.map (λList.map: {α β : Type} → (α → β) → List α → List βn =>n: Natn +n: Nat1) [1: Nat0,0: Nat1,1: Nat2,2: Nat3,3: Nat4]4: NatList.mapList.map: {α β : Type} → (α → β) → List α → List βString.length [String.length: String → Nat"I","I": String"Love","Love": String"Logic!"]"Logic!": String
List.foldr
The foldr function converts a binary operation along with its identity element into a generalized n-ary operation that takes any number of arguments, in a list. As an example, our reduce_or function, taking a list of Bools and reducing it to just one, indicating whether the list has at least one true value, is simply an n-ary extension of or. Applying such an n-ary operation on no arguments (an empty list) simply returns the identity element (base case value).
@List.foldrList.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → βList.foldrList.foldr: {α β : Type} → (α → β → β) → β → List α → βNat.addNat.add: Nat → Nat → Nat0 [0: Nat1,1: Nat2,2: Nat3,3: Nat4,4: Nat5] -- expect 155: NatList.foldrList.foldr: {α β : Type} → (α → β → β) → β → List α → βNat.mulNat.mul: Nat → Nat → Nat0 [0: Nat1,1: Nat2,2: Nat3,3: Nat4,4: Nat5] -- expect 120, oops!5: NatList.foldrList.foldr: {α β : Type} → (α → β → β) → β → List α → βNat.mulNat.mul: Nat → Nat → Nat1 [1: Nat1,1: Nat2,2: Nat3,3: Nat4,4: Nat5] -- expect 120, ah!5: Nat
List.filter
The List.filter function takes a list, l of α values, and an α → Bool predicate function that indicates whether a given α value has a particular property, and returns the sublist of α values in l that have property, p.
@List.filterList.filter: {α : Type u_1} → (α → Bool) → List α → List αList.filter (λ (List.filter: {α : Type} → (α → Bool) → List α → List αn :n: NatNat) =>Nat: Typen%n: Nat2 ==2: Nat0) [0: Nat0,0: Nat1,1: Nat2,2: Nat3,3: Nat4,4: Nat5,5: Nat6,6: Nat7]7: Nat
Propositional Logic: The Next Generation
Here again is our definition of the syntax and semantics of propositional logic, now supporting all the connectives, including ⇔. There's little additional information here to review, so you may skim this section quickly.
Syntax
structure var: Type
var : Type: Type 1
Type := (n: var → Nat
n: Nat: Type
Nat)
inductive unary_op: Type
unary_op : Type: Type 1
Type | not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
| imp: binary_op
imp
| iff: binary_op
iff
inductive Expr: Type
Expr : Type: Type 1
Type
| true_exp: Expr
true_exp
| false_exp: Expr
false_exp
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
notation "{"v: Lean.TSyntax `term
v"}" => Expr.var_exp: var → Expr
Expr.var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.imp: binary_op
binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.iff: binary_op
binary_op.iff
notation " ⊤ " => Expr.top_exp: Type
Expr.top_exp
notation " ⊥ " => Expr.bot_exp: Type
Expr.bot_exp
Semantics
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def implies: Bool → Bool → Bool
implies : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, false: Bool
false => false: Bool
false
| _, _ => true: Bool
true
def iff: Bool → Bool → Bool
iff : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, true: Bool
true => true: Bool
true
| false: Bool
false, false: Bool
false => true: Bool
true
| _, _ => false: Bool
false
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
| binary_op.imp: binary_op
binary_op.imp => implies: Bool → Bool → Bool
implies
| binary_op.iff: binary_op
binary_op.iff => iff: Bool → Bool → Bool
iff
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool
-- main semantic evaluation function
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| Expr.true_exp: Expr
Expr.true_exp, _ => true: Bool
true
| Expr.false_exp: Expr
Expr.false_exp, _ => false: Bool
false
| (Expr.var_exp: var → Expr
Expr.var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Satisfiability Properties
Next we present an improved version of or code for checking of expressions for validity, satisfiability, and unsatisfiability.
One significant enhancement, suggested by Mikhail, is replacement of our rather ponderous approach to generating the input sides of truth tables with a single recursive function. We also use our new map, filter, and reduce functions to replace numerous specialized instances.
Truth Table Input Rows
We had previousl developed an explanatory but ponderous approach to generating a list of of all lists of boolean input rows. The idea was to treat the each (input) row as a binary expansion of the row index (a lit of bit), convert bits to bools, and add padding on the left. Mikhail noticed that we could replace it all with a single recursive function.
Exercise: Study this function definition until you understand fully how it works. Along the way, use it to generate a few outputs then inspect them to be sure you know what the function does. Figure out the recursion works to the point you're confident you could write the code yourself. To test yourself, erase the implementation then write it again.
-- Mikhail defmake_bool_lists:make_bool_lists: Nat → List (List Bool)Nat →Nat: TypeList (List: Type → TypeListList: Type → TypeBool) |Bool: Type0 => [0: Nat[]] |[]: List Booln' + 1 => (n': NatList.map (funList.map: {α β : Type} → (α → β) → List α → List βL =>L: List Boolfalse::false: BoolL) (L: List Boolmake_bool_listsmake_bool_lists: Nat → List (List Bool)n')) ++ (n': NatList.map (funList.map: {α β : Type} → (α → β) → List α → List βL =>L: List Booltrue::true: BoolL) (L: List Boolmake_bool_listsmake_bool_lists: Nat → List (List Bool)n')) -- REVIEWn': Natmake_bool_listsmake_bool_lists: Nat → List (List Bool)00: Natmake_bool_listsmake_bool_lists: Nat → List (List Bool)11: Natmake_bool_listsmake_bool_lists: Nat → List (List Bool)22: Natmake_bool_listsmake_bool_lists: Nat → List (List Bool)33: Nat
Bool List to/from Interpretation Function
Given a list of n Boolean values, [b₀, ..., bₙ₋₁], we have to be able to turn it into an interpretation function, so that we can evaluate expressions with that interpretation using eval_expr. The resulting function will be { v₀ ↦ b₀, ..., vₙ₋₁ ↦ bₙ₋₁}, where each vᵢ means (var.mk i).
Our approach will be to start with a given interpretation (such as the all false interpretation) and then for each bᵢ in the list of Booleans, we will iteratively override the function so that when it's used to evaluate the value of vᵢ it will return bᵢ.
-- Function override
def override: Interp → var → Bool → Interp
override : Interp: Type
Interp → var: Type
var → Bool: Type
Bool → Interp: Type
Interp
| old_interp: Interp
old_interp, var: _root_.var
var, new_val: Bool
new_val =>
(λ v: _root_.var
v => if (v: _root_.var
v.n: _root_.var → Nat
n == var: _root_.var
var.n: _root_.var → Nat
n) -- when applied to var
then new_val: Bool
new_val -- return new value
else old_interp: Interp
old_interp v: _root_.var
v) -- else retur old value
-- Bool list to interpretation function
-- Uses list length as number of variables to associate with bools
def bool_list_to_interp: List Bool → Interp
bool_list_to_interp : List: Type → Type
List Bool: Type
Bool → Interp: Type
Interp
| l: List Bool
l => bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper l: List Bool
l.length: {α : Type} → List α → Nat
length l: List Bool
l
where bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper : (vars: Nat
vars : Nat: Type
Nat) → (vals: List Bool
vals : List: Type → Type
List Bool: Type
Bool) → Interp: Type
Interp
| _, [] => (λ _: var
_ => false: Bool
false)
| vars: Nat
vars, h: Bool
h::t: List Bool
t =>
let len: Nat
len := (h: Bool
h::t: List Bool
t).length: {α : Type} → List α → Nat
length
-- override recursively computed interp mapping variable to head bool
override: Interp → var → Bool → Interp
override (bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper vars: Nat
vars t: List Bool
t) (var.mk: Nat → var
var.mk (vars: Nat
vars - len: Nat
len)) h: Bool
h
To think about: smells like some kind of fold. Iteratively combine bool at head of list with given interpretation by overriding at with the h ead value for the which? variable
In addition to converting Boolean lists to interpretations it will also be useful to turn interpretations back into Boolean lists, where the length of each list is typically fixed at a specified number of variables (all variables beyond a certain point being irrelevant to a given expression).
-- From number of variables, interpretation, to list of Bools
def interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool : (num_vars: Nat
num_vars : Nat: Type
Nat) → Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| 0: Nat
0, _ => []: List Bool
[]
| (n': Nat
n' + 1) , i: Interp
i => interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool n': Nat
n' i: Interp
i ++ [(i: Interp
i (var.mk: Nat → var
var.mk n': Nat
n'))]
-- From number of variables, list of interpretations, to list of Bool lists
def interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| vars: Nat
vars, is: List Interp
is => List.map: {α β : Type} → (α → β) → List α → List β
List.map (interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool vars: Nat
vars) is: List Interp
is
Maximum Variable Index in Expression
We will consider the number of variables to include in a truth table for a given expression to be the one plus the zero-based index of the highest-indexed variable in any given expression. For example, if an expression uses only v₉ explicitly we will consider it to use all ten variables, v₀ to v₉ inclusive.
def max_variable_index: Expr → Nat
max_variable_index : Expr: Type
Expr → Nat: Type
Nat
| Expr.true_exp: Expr
Expr.true_exp => 0: Nat
0
| Expr.false_exp: Expr
Expr.false_exp => 0: Nat
0
| Expr.var_exp: var → Expr
Expr.var_exp (var.mk: Nat → var
var.mk i: Nat
i) => i: Nat
i
| Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp _ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e
| Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp _ e1: Expr
e1 e2: Expr
e2 => max: {α : Type} → [self : Max α] → α → α → α
max (max_variable_index: Expr → Nat
max_variable_index e1: Expr
e1) (max_variable_index: Expr → Nat
max_variable_index e2: Expr
e2)
Number of Variables in Expression
We take the number of variables in an expression to be the index of the highest-indexed variable in the expression, plus one to account for the usual zero-based indexing.
def num_vars: Expr → Nat
num_vars : Expr: Type
Expr → Nat: Type
Nat := λ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e + 1: Nat
1
From Expression to List of Interpretations
Given an expression, we compute the number, n, of variables it uses then we generate a list of all 2^n interpretation functions for it. Note that we just eliminate a whole raft of ponderous code with a single clever recursive function, thanks to Mikhail.
-- Number of variables to interpretations list using Mikhail's code
def mk_interps_vars: Nat → List Interp
mk_interps_vars : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp
| n: Nat
n => List.map: {α β : Type} → (α → β) → List α → List β
List.map bool_list_to_interp: List Bool → Interp
bool_list_to_interp (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n)
-- From expression to a list of interpretations for it
def mk_interps_expr: Expr → List Interp
mk_interps_expr : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp
| e: Expr
e => mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e)
Truth Table Outputs
Exercise: Replace the following definition of truth_table_outputs with a single line of code using List.map. The resulting list of Boolean values should reflect the values of the given expression under each interpretation in the list of interpretations. You will use map to convert a list of interpretations (for e) into a list of Boolean values.
-- The column of truth table outputs for e
def truth_table_outputs': Expr → List Bool
truth_table_outputs' : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e (mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e))
where eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| _, [] => []: List Bool
[]
| e: Expr
e, h: Interp
h::t: List Interp
t => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e t: List Interp
t ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
-- REVIEW
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => List.map: {α β : Type} → (α → β) → List α → List β
List.map (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e) (mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e))
-- | e => eval_expr_over_interps e (mk_interps_vars (num_vars e))
-- where eval_expr_over_interps : Expr → List Interp → List Bool
-- | _, [] => []
-- | e, h::t => eval_expr_over_interps e t ++ [eval_expr e h]
n-ary And and Or functions
def reduce_or: List Bool → Bool
reduce_or := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr or: Bool → Bool → Bool
or false: Bool
false
def reduce_and: List Bool → Bool
reduce_and := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr and: Bool → Bool → Bool
and true: Bool
true
Satisfiability-Related Properties of Expressions
Finally we can define the API we want to provide for checking arbitrary propositional logic expressions for their satisfiability properties: for being satisfiable, valid, or unsatisfiable.
def is_sat: Expr → Bool
is_sat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_or: List Bool → Bool
reduce_or (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_valid: Expr → Bool
is_valid (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_and: List Bool → Bool
reduce_and (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_unsat: Expr → Bool
is_unsat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := not: Bool → Bool
not (is_sat: Expr → Bool
is_sat e: Expr
e)
Models and Counterexamples
We now turn to the third and last major topic in this chapter. Given a propositional logic expression, e, a model finder finds a model of e if there is one. It returns either a model of e if there is one or a signal that there isn't one.
To return either a model if there is one or a signal that there isn't one, we could use a sum type: either a model on the left or Unit.unit on the right to signal that there is no model.
def SomeInterpOrNone: Type
SomeInterpOrNone := Interp: Type
Interp ⊕ Unit: Type
Unit -- NB: this is a *type*
A better solution is to use the standard polymorphic Option type. Its two constructors are some α and none. The first is used to construct an option carrying a value, (a : α). The second is used (in lieu of Sum.inr Unit.unit) to indicate that there's no value to provide.
def o1: Option Bool
o1 := Option.some: {α : Type} → α → Option α
Option.some true: Bool
true
def o2: Option Bool
o2 := @Option.none: {α : Type} → Option α
Option.none Bool: Type
Bool -- need to make type argument explicit
Model Finder
Here's the main API for our model finder. Given an expression, e, return some m, m a model of e if there is one, or none if not.
@Option defOption: Type u_1 → Type u_1find_model :find_model: Expr → Option InterpExpr →Expr: TypeOptionOption: Type → TypeInterp |Interp: Typee => lete: Exprinterps :=interps: List Interpmk_interps_exprmk_interps_expr: Expr → List Interpee: Exprfind_model_helperfind_model_helper: List Interp → Expr → Option Interpinterpsinterps: List Interpe wheree: Exprfind_model_helper :find_model_helper: List Interp → Expr → Option InterpListList: Type → TypeInterp →Interp: TypeExpr →Expr: TypeOptionOption: Type → TypeInterp | [], _ =>Interp: Typenone |none: {α : Type} → Option αh::h: Interpt,t: List Interpe => if (e: Expreval_expreval_expr: Expr → Interp → Boolee: Exprh) thenh: Interpsomesome: {α : Type} → α → Option αh elseh: Interpfind_model_helperfind_model_helper: List Interp → Expr → Option Interptt: List Interpe -- REVIEW -- Utility: convert a "Option model" into a list of Bools, empty for none defe: Exprsome_model_or_none_to_bools :some_model_or_none_to_bools: SomeInterpOrNone → Nat → List BoolSomeInterpOrNone → (SomeInterpOrNone: Typenum_vars :num_vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: TypeSum.inlSum.inl: {α : Type ?u.57773} → {β : Type ?u.57772} → α → α ⊕ βi,i: Interpn =>n: Natinterp_to_list_boolinterp_to_list_bool: Nat → Interp → List Boolnn: Nati |i: InterpSum.inr _, _ =>Sum.inr: {α : Type ?u.57803} → {β : Type ?u.57802} → β → α ⊕ β[][]: List Bool
Model Enumerator
The main API of our model enumeration section is the function, find_models, that takes an expression, e,, and returns a list of all models of e. It does so by generating an exhaustive list of all interpretations then filtering them to save those that make e true.
def find_models: Expr → List Interp
find_models (e: Expr
e : Expr: Type
Expr) :=
List.filter: {α : Type} → (α → Bool) → List α → List α
List.filter -- filter on
(λ i: Interp
i => eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i) -- i makes e true
(mk_interps_expr: Expr → List Interp
mk_interps_expr e: Expr
e) -- over all interps
-- Render models of e : Expr as List of Bool Lists (num_vars e long)
def find_models_bool: Expr → List (List Bool)
find_models_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_models: Expr → List Interp
find_models e: Expr
e)
Model Counter
A model counter takes an expression and tells you how many models it has. From a list of all models, it's obviously easy to derive the number of models: it's just the length of the list. Note that we use function composition to define our model counting function.
def count_models: Expr → Nat
count_models := List.length: {α : Type} → List α → Nat
List.length ∘ find_models: Expr → List Interp
find_models
Counter-Example Generator
More interesting, and oft used, is counter-example finding. When we say we want to disprove a proposition, mean is that we want to show that it's not valid: that there's at least one interpretation that makes the proposition is false. If that is so, then it makes the negation of the proposition true. Counterexamples, if there are any, are models of the negation of a propositio; and we now know how to find such models using our model finder. Defining a counter-example finder is thus trivial.
def find_counterexamples: Expr → List Interp
find_counterexamples (e: Expr
e : Expr: Type
Expr) := find_models: Expr → List Interp
find_models (¬e: Expr
e)
def find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_counterexamples: Expr → List Interp
find_counterexamples e: Expr
e)
Tests and Demonstrations
defX := {X: Exprvar.mkvar.mk: Nat → var0} def0: NatY := {Y: Exprvar.mkvar.mk: Nat → var1} def1: NatZ := {Z: Exprvar.mkvar.mk: Nat → var2}2: Nattruth_table_outputs (truth_table_outputs: Expr → List BoolX ∧X: ExprY)Y: ExprList.foldrList.foldr: {α β : Type} → (α → β → β) → β → List α → βoror: Bool → Bool → Boolfalse (false: Booltruth_table_outputs (truth_table_outputs: Expr → List BoolX ∧X: ExprY))Y: ExprList.foldrList.foldr: {α β : Type} → (α → β → β) → β → List α → βandand: Bool → Bool → Booltrue (true: Booltruth_table_outputs (truth_table_outputs: Expr → List BoolX ∧X: ExprY))Y: Expr
Is it true that if X being true makes Y true, then does X being false make Y false?
((X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expr(implies (implies: Bool → Bool → Boolimpliesimplies: Bool → Bool → Boolfalsefalse: Booltrue) (true: Boolimpliesimplies: Bool → Bool → Booltruetrue: Boolfalse))false: Bool
Is it true that if X being true means that Y must be true, then does Y being false imply X is false?
((X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Expr
We can find all the models of an expression.
find_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expr
Simple model counting.
count_models (count_models: Expr → NatX ∨X: ExprY)Y: Exprcount_models (count_models: Expr → NatX ∧X: ExprY)Y: Expr
Search for models (returns list of functions)
find_models (find_models: Expr → List InterpX ∧ ¬X: ExprX) -- expect []X: Expr(find_models (find_models: Expr → List InterpX ∨X: ExprY)).Y: Exprlength -- expect 3length: {α : Type} → List α → Nat(find_models (find_models: Expr → List InterpX ∧X: ExprY)).Y: Exprlength -- expect 1length: {α : Type} → List α → Nat
Search for models (returns list of list of bools)
find_models_bool (find_models_bool: Expr → List (List Bool)X ∧ ¬X: ExprX) -- []X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∨ ¬X: ExprX) -- [[false], [true]X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∧X: ExprY) -- [[true, true]]Y: Exprfind_models_bool (¬(find_models_bool: Expr → List (List Bool)X ∧X: ExprY) ⇒ ¬Y: ExprX ∨ ¬X: ExprY) -- all four interpsY: Exprfind_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (Y: ExprY ⇒Y: ExprZ) ⇒ (Z: ExprX ⇒X: ExprZ)) -- all eight interpsZ: Expr
Homework
Forthcoming:
- Expand make_bool_lists applied to values 0-3.
- Validate a list of standard inference rules.
- Find the Fallacies, Explain Counterexamples.
- Replace ponderous function definition using map.
Satifiability Modulo Theories
UNDER CONSTRUCTION.
At the end of the last chapter, we saw first-hand the magnificence of automated satisfiability and validity checking for propositional logic. Most recently we met model-finding algorithms. As usual lately, this chapter starts by presenting an updated and compressed version of our specifications for proposition logic, properties of expressions, and model finding. We'll briefly review this material at the start of class. We'll then turn to our main new topic: satisfiability modulo theories.
Review and Extensions
Higher-order functions in lists
@List.mapList.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β@List.foldrList.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β@List.filterList.filter: {α : Type u_1} → (α → Bool) → List α → List α
Language of Propositional Logic
structure var: Type
var : Type: Type 1
Type := (n: var → Nat
n: Nat: Type
Nat)
inductive unary_op: Type
unary_op : Type: Type 1
Type | not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
| imp: binary_op
imp
| iff: binary_op
iff
inductive Expr: Type
Expr : Type: Type 1
Type
| true_exp: Expr
true_exp
| false_exp: Expr
false_exp
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
notation "{"v: Lean.TSyntax `term
v"}" => Expr.var_exp: var → Expr
Expr.var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.imp: binary_op
binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.iff: binary_op
binary_op.iff
notation " ⊤ " => Expr.top_exp: Type
Expr.top_exp
notation " ⊥ " => Expr.bot_exp: Type
Expr.bot_exp
def implies: Bool → Bool → Bool
implies : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, false: Bool
false => false: Bool
false
| _, _ => true: Bool
true
def iff: Bool → Bool → Bool
iff : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, true: Bool
true => true: Bool
true
| false: Bool
false, false: Bool
false => true: Bool
true
| _, _ => false: Bool
false
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
| binary_op.imp: binary_op
binary_op.imp => implies: Bool → Bool → Bool
implies
| binary_op.iff: binary_op
binary_op.iff => iff: Bool → Bool → Bool
iff
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| Expr.true_exp: Expr
Expr.true_exp, _ => true: Bool
true
| Expr.false_exp: Expr
Expr.false_exp, _ => false: Bool
false
| (Expr.var_exp: var → Expr
Expr.var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Interpretations and Truth Tables
def reduce_or: List Bool → Bool
reduce_or := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr or: Bool → Bool → Bool
or false: Bool
false
def reduce_and: List Bool → Bool
reduce_and := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr and: Bool → Bool → Bool
and true: Bool
true
def make_bool_lists: Nat → List (List Bool)
make_bool_lists: Nat: Type
Nat → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| 0: Nat
0 => [[]: List Bool
[]]
| n: Nat
n + 1 => (List.map: {α β : Type} → (α → β) → List α → List β
List.map (fun L: List Bool
L => false: Bool
false::L: List Bool
L) (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n)) ++
(List.map: {α β : Type} → (α → β) → List α → List β
List.map (fun L: List Bool
L => true: Bool
true::L: List Bool
L) (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n))
def override: Interp → var → Bool → Interp
override : Interp: Type
Interp → var: Type
var → Bool: Type
Bool → Interp: Type
Interp
| old_interp: Interp
old_interp, var: _root_.var
var, new_val: Bool
new_val =>
(λ v: _root_.var
v => if (v: _root_.var
v.n: _root_.var → Nat
n == var: _root_.var
var.n: _root_.var → Nat
n)
then new_val: Bool
new_val
else old_interp: Interp
old_interp v: _root_.var
v)
def bool_list_to_interp: List Bool → Interp
bool_list_to_interp : List: Type → Type
List Bool: Type
Bool → Interp: Type
Interp
| l: List Bool
l => bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper l: List Bool
l.length: {α : Type} → List α → Nat
length l: List Bool
l
where bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper : (vars: Nat
vars : Nat: Type
Nat) → (vals: List Bool
vals : List: Type → Type
List Bool: Type
Bool) → Interp: Type
Interp
| _, [] => (λ _: var
_ => false: Bool
false)
| vars: Nat
vars, h: Bool
h::t: List Bool
t =>
let len: Nat
len := (h: Bool
h::t: List Bool
t).length: {α : Type} → List α → Nat
length
override: Interp → var → Bool → Interp
override (bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper vars: Nat
vars t: List Bool
t) (var.mk: Nat → var
var.mk (vars: Nat
vars - len: Nat
len)) h: Bool
h
def interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool : (num_vars: Nat
num_vars : Nat: Type
Nat) → Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| 0: Nat
0, _ => []: List Bool
[]
| (n': Nat
n' + 1) , i: Interp
i => interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool n': Nat
n' i: Interp
i ++ [(i: Interp
i (var.mk: Nat → var
var.mk n': Nat
n'))]
def interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| vars: Nat
vars, is: List Interp
is => List.map: {α β : Type} → (α → β) → List α → List β
List.map (interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool vars: Nat
vars) is: List Interp
is
def max_variable_index: Expr → Nat
max_variable_index : Expr: Type
Expr → Nat: Type
Nat
| Expr.true_exp: Expr
Expr.true_exp => 0: Nat
0
| Expr.false_exp: Expr
Expr.false_exp => 0: Nat
0
| Expr.var_exp: var → Expr
Expr.var_exp (var.mk: Nat → var
var.mk i: Nat
i) => i: Nat
i
| Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp _ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e
| Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp _ e1: Expr
e1 e2: Expr
e2 => max: {α : Type} → [self : Max α] → α → α → α
max (max_variable_index: Expr → Nat
max_variable_index e1: Expr
e1) (max_variable_index: Expr → Nat
max_variable_index e2: Expr
e2)
def mk_interps_vars: Nat → List Interp
mk_interps_vars : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp
| n: Nat
n => List.map: {α β : Type} → (α → β) → List α → List β
List.map bool_list_to_interp: List Bool → Interp
bool_list_to_interp (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n)
-- main api
def num_vars: Expr → Nat
num_vars : Expr: Type
Expr → Nat: Type
Nat := λ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e + 1: Nat
1
def mk_interps_expr: Expr → List Interp
mk_interps_expr : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp
| e: Expr
e => mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e)
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e (mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e))
where eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| _, [] => []: List Bool
[]
| e: Expr
e, h: Interp
h::t: List Interp
t => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e t: List Interp
t ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
Satisfiability Properties of Expressions
def is_sat: Expr → Bool
is_sat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_or: List Bool → Bool
reduce_or (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_valid: Expr → Bool
is_valid (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_and: List Bool → Bool
reduce_and (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_unsat: Expr → Bool
is_unsat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := not: Bool → Bool
not (is_sat: Expr → Bool
is_sat e: Expr
e)
Finding Models and Counterexamples
def find_model: Expr → Option Interp
find_model : Expr: Type
Expr → Option: Type → Type
Option Interp: Type
Interp
| e: Expr
e =>
let interps: List Interp
interps := mk_interps_expr: Expr → List Interp
mk_interps_expr e: Expr
e
find_model_helper: List Interp → Expr → Option Interp
find_model_helper interps: List Interp
interps e: Expr
e
where find_model_helper: List Interp → Expr → Option Interp
find_model_helper : List: Type → Type
List Interp: Type
Interp → Expr: Type
Expr → Option: Type → Type
Option Interp: Type
Interp
| [], _ => none: {α : Type} → Option α
none
| h: Interp
h::t: List Interp
t, e: Expr
e => if (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h) then some: {α : Type} → α → Option α
some h: Interp
h else find_model_helper: List Interp → Expr → Option Interp
find_model_helper t: List Interp
t e: Expr
e
def find_models: Expr → List Interp
find_models (e: Expr
e : Expr: Type
Expr) :=
List.filter: {α : Type} → (α → Bool) → List α → List α
List.filter -- filter on
(λ i: Interp
i => eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i) -- i makes e true
(mk_interps_expr: Expr → List Interp
mk_interps_expr e: Expr
e) -- over all interps
def find_models_bool: Expr → List (List Bool)
find_models_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_models: Expr → List Interp
find_models e: Expr
e)
def count_models: Expr → Nat
count_models := List.length: {α : Type} → List α → Nat
List.length ∘ find_models: Expr → List Interp
find_models
def find_counterexamples: Expr → List Interp
find_counterexamples (e: Expr
e : Expr: Type
Expr) := find_models: Expr → List Interp
find_models (¬e: Expr
e)
def find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_counterexamples: Expr → List Interp
find_counterexamples e: Expr
e)
Examples and Practice
defX := {X: Exprvar.mkvar.mk: Nat → var0} def0: NatY := {Y: Exprvar.mkvar.mk: Nat → var1} def1: NatZ := {Z: Exprvar.mkvar.mk: Nat → var2} -- If X being true makes Y true, then does X being false make Y false?2: Nat((X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expr(implies (implies: Bool → Bool → Boolimpliesimplies: Bool → Bool → Boolfalsefalse: Booltrue) (true: Boolimpliesimplies: Bool → Bool → Booltruetrue: Boolfalse)) -- If X implies Y, then does not Y false imply not X?false: Bool((X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX)) -- Find all the models of an expression.X: Exprfind_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY)) -- Simple model counting.Y: Exprcount_models (count_models: Expr → NatX ∨X: ExprY)Y: Exprcount_models (count_models: Expr → NatX ∧X: ExprY) -- Find all models, return list of functionsY: Exprfind_models (find_models: Expr → List InterpX ∧ ¬X: ExprX) -- expect []X: Expr(find_models (find_models: Expr → List InterpX ∨X: ExprY)).Y: Exprlength -- expect 3length: {α : Type} → List α → Nat(find_models (find_models: Expr → List InterpX ∧X: ExprY)).Y: Exprlength -- expect 1 -- Find all models, returns list of bool listslength: {α : Type} → List α → Natfind_models_bool (find_models_bool: Expr → List (List Bool)X ∧ ¬X: ExprX) -- []X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∨ ¬X: ExprX) -- [[false], [true]X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∧X: ExprY) -- [[true, true]]Y: Exprfind_models_bool (¬(find_models_bool: Expr → List (List Bool)X ∧X: ExprY) ⇒ ¬Y: ExprX ∨ ¬X: ExprY) -- all four interpsY: Exprfind_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (Y: ExprY ⇒Y: ExprZ) ⇒ (Z: ExprX ⇒X: ExprZ)) -- all eight interpsZ: Expr
Satisfiability Modulo Theories
So far we have specified software that solves (finds models of) propositions in pure propositional logic. But this logic is esoecially austere. Variables can have only Boolean values, and the operators are all Boolean.
What makes propositional logic much more useful is to allow atomic expressions (variable expressions) to be expanded into expressions in other formal languages. For example, expanding the variables in X and Y in the expression X ∧ Y into arithmetic expressions, we could write the following proposition: X > 0 ∧ Y = 2 * X, with X and Y ranging over the natural numbers.
In this logic, interpretations are extended to associate values of types other than Boolean with variables. Model finding then involves finding values of such variables, e.g., integer-valued variables, that make an expression true. Here a model (solution) would be { X = 1, Y = 2 }.
A Game: You're the Finder. What's the smallest integer value for X and a corresponding integer value for Y that make this proposition true: (X > Y + 2) ∧ (Y ≤ 7)?
-- Your answer here:
The Z3 SMT Solver via a Python API
So let's crank up Z3! Open lecture_15.py. Read and and run it using the run icon at the top of the Python editing panel. You should be able to run it by clicking the Python run icon.
Find Python Z3 documentation here. Then continue to follow instructions for readings and activities given elsewhere to continue with this chapter. Return here when done.
Function Terms as Free Variables
We assume you've now seen how to read, write, and solve propositional logic and arithmetic constraints, and have seen enough examples to know what it feels like to have Z3 find solutions without the need to write problem-specific procedural code.
Encoding Problems in SMT for Automated Model Search
Currently as quoted from Z3 Python Tutorial Python files.
Cats Mice Dogs
Sudoku
Eight Queens
Curry-Howard: Logic as Computation
ToDo: More explanation.
Empty ↦ False
Empty
You've already met and understood the Empty data type.
EmptyEmpty: Type
inductive Empty : Type
As an example, here's another uninhabited type (in Type)
inductive Chaos: Type
Chaos : Type: Type 1
Type
From an assumption that one has a value of type Empty, anything can follow. We can even promise to return a value of our new uninhabited type.
def from_empty: Empty → Chaos
from_empty (e: Empty
e : Empty: Type
Empty) : Chaos: Type
Chaos := nomatch e: Empty
e
False
The logical analog of the Empty data type is the proposition, False. It is an uninhabited type, but now in Prop. Such a type is understood as representing a proposition. That there is no proof of False---no value of this type---means that as a proposition it is logically false.
FalseFalse: Prop
inductive False : Prop
def from_false: ∀ {P : Prop}, False → P
from_false {P: Prop
P : Prop: Type
Prop} (p: False
p : False: Prop
False) : P: Prop
P := False.elim: ∀ {C : Prop}, False → C
False.elim p: False
p
def from_false_true_is_false: False → True = False
from_false_true_is_false (p: False
p : False: Prop
False) : True: Prop
True = False: Prop
False := False.elim: ∀ {C : Prop}, False → C
False.elim p: False
p
-- no introduction rule, as there are no proofs of False
Unit ↦ True
Unit
Unit -- inductive PUnit : Sort u where -- | unit : PUnitUnit: Type
True
TrueTrue: Prop
inductive True : Prop where | intro : True
True.intro -- no elimination rule defTrue.intro: Trueproof_of_true :proof_of_true: TrueTrue :=True: PropTrue.introTrue.intro: True
Example
def false_implies_true: False → Chaos
false_implies_true : False: Prop
False → Chaos: Type
Chaos :=
λ f: False
f => False.elim: {C : Type} → False → C
False.elim f: False
f
Prod ↦ And
Prod
Prod /- structure Prod (α : Type u) (β : Type v) where fst : α snd : β -/Prod: Type u → Type v → Type (max u v)
And
And /- structure And (a b : Prop) : Prop where intro :: left : a right : b -/ -- Propositions as types, proofs as values inductiveAnd: Prop → Prop → PropBirds_chirping :Birds_chirping: PropProp |Prop: Typeyep |yep: Birds_chirpingboo -- Propositions as types, proofs as values inductiveboo: Birds_chirpingSky_blue :Sky_blue: PropProp |Prop: Typeyepyep: Sky_blue(AndAnd: Prop → Prop → PropBirds_chirpingBirds_chirping: PropSky_blue)Sky_blue: Prop(Birds_chirping ∧Birds_chirping: PropSky_blue) theoremSky_blue: Propa_proof :a_proof: Birds_chirping ∧ Sky_blueBirds_chirping ∧Birds_chirping: PropSky_blue := -- And.intro Birds_chirping.yep Sky_blue.yep ⟨Sky_blue: PropBirds_chirping.yep,Birds_chirping.yep: Birds_chirpingSky_blue.yep ⟩ -- notationSky_blue.yep: Sky_blue
On Proof Irrelevance
For the purpose of demonstrating that a given proposition is true (or, more accurately, valid), any proof will do. All proofs are equivalent in this dimension. In Prop, all proof values are considered to be equal. Moreover, choices among otherwise equivalent proofs aren't allowed to affect rsults of computations.
namespace cs2120f23
With values of data types, we care a lot about particular values. There's a huge difference between tre and false as values of the Boolean type,
Indeed, one of the fundamental rules of inductive data type definitions (in Type or above) is that constructors are disjoint. This means that different constructors always create values that are different: unequal.
inductive Bool: Type
Bool : Type: Type 1
Type
| true: Bool
true
| false: Bool
false
But because Birds_chirping is in Prop (it's a proposition, right) all of its values, all values accepted as proofs of the propisition, are actually considered to be equal. You would understand the details of the formal proof until we talk about equality, but you can trust that Lean is accepting that there is a proof that boo and yep really are equal.
theoremproof_equal :proof_equal: Birds_chirping.boo = Birds_chirping.yepBirds_chirping.boo =Birds_chirping.boo: Birds_chirpingBirds_chirping.yep :=Birds_chirping.yep: Birds_chirpingGoals accomplished! 🐙Goals accomplished! 🐙
Major take-away: values of propositional types are not just all equally acceptable as mathematical proof objects, they are considered as all being literally equal. We will talk about equality in more detail soon.
Sum ↦ Or
Sum Data Type
Sum /- inductive Sum (α : Type u) (β : Type v) where | inl (val : α) : Sum α β | inr (val : β) : Sum α β -/Sum: Type u → Type v → Type (max u v)
Or Connective
Or /- inductive Or (a b : Prop) : Prop where | inl (h : a) : Or a b | inr (h : b) : Or a b -/ -- Two different proofs of the same proposition, theoremOr: Prop → Prop → Propone_or_other :one_or_other: Birds_chirping ∨ Sky_blueOrOr: Prop → Prop → PropBirds_chirpingBirds_chirping: PropSky_blue :=Sky_blue: PropOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ bBirds_chirping.yep theoremBirds_chirping.yep: Birds_chirpingone_or_other' :one_or_other': Birds_chirping ∨ Sky_blueOrOr: Prop → Prop → PropBirds_chirpingBirds_chirping: PropSky_blue :=Sky_blue: PropOr.inrOr.inr: ∀ {a b : Prop}, b → a ∨ bSky_blue.yepSky_blue.yep: Sky_blueexample :example: one_or_other = one_or_other'one_or_other =one_or_other: Birds_chirping ∨ Sky_blueone_or_other' :=one_or_other': Birds_chirping ∨ Sky_bluerfl -- the different proofs are equalrfl: ∀ {α : Prop} {a : α}, a = a
In some cases you'll need to select the disjuct for which you have a proof.
example :example: Birds_chirping ∨ 0 = 1OrOr: Prop → Prop → PropBirds_chirping (Birds_chirping: Prop0=0: Nat1) :=1: NatOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ bBirds_chirping.yep -- inr is no goBirds_chirping.yep: Birds_chirpingexample : (example: 0 = 1 ∨ 1 = 20=0: Nat1) ∨ (1: Nat1=1: Nat2):=2: Nat-- no proof of either disjunct: false
We know have ample machinery to prove interesting theorems in fundamental mathematical logic. As an example, we now state and construct a proof of the proposition that Or is commutative. Technically it's a biimplication, but the cases are symmetric so we'll just consider one direction: if we assume that P and Q are arbitrary propositions, then if the proposition P ∨ Q is true (and we have a proof, poq) then we can derive a proof of Q ∨ P, showing that it is true, and that our overall proposition is valid. The proof of that is by case analysis
theorem or_comm {P Q : Prop} : P ∨ Q → Q ∨ P :=
λ (poq : P ∨ Q) =>
match poq with
| Or.inl p => Or.inr p
| Or.inr q => Or.inl q
Negation
no
When representing logical operations using computational types (in Type), we represented the proof of a negation of a proposition, α, as a function (implementation) from (of type) α → Empty.
If there is a function of this type, then the type, α, must be uninhabited: there are no proofs of it; so it is false and the proposition ¬α is true. To prove ¬α, give a function of type α → Empty. This is all of course when implementing logical reasoning in using computational (Sum, Prod, Empty, and other) types (in Type).
def no (α : Type) : Type := α → Empty
Example. Recall we defined Chaos as uninhabited in Type. This is how we model the notion that Choas is false. We no Chaos literally means (the proposition-representing function type) Chaos → Empty. What we do in this example is to show that there is a value of this function type. The function takes a Chaos value (proof) as an argument., for which an empty case analysis satisfies the obligation of the function to return a value in each case.
example : no Chaos := λ c => nomatch c
Not(¬)
We'll now see that the approach is analogous in Prop, the type Universe for logical reasoning in Lean. If P is any proposition, then Not P (concrete notation, ¬P) is also a proposition. It is true when P (the type of proofs of P) is uninhabited.
NotNot: Prop → Prop
def Not (a : Prop) : Prop := a → False
Compare this directly and carefully with how we defined the corresponding concept, no, using computational types and values (in or under Type).
-- Computational
example: no Chaos
example : no: Type → Type
no Chaos: Type
Chaos := λ (c: Chaos
c : Chaos: Type
Chaos) => nomatch c: Chaos
c
-- Logical
-- A proposition with no proofs is false
-- Here then is a proposition, it's raining, that's false
inductive Raining: Prop
Raining : Prop: Type
Prop
-- What's cool is that we can now prove it's negation (is valid)
example: ¬Raining
example : ¬Raining: Prop
Raining := λ (r: Raining
r : Raining: Prop
Raining) => nomatch r: Raining
r
-- Compare this with a corresponding example in TypeExamples and Excluded Middle
Examples
We begin with examples to review and reinforce lessons so far.
¬False is Valid (True Under Any Interpretation)
This example will teach and reinforce the idea that, to prove ¬P, for any proposition P, you show that an assumed proof of P leads to a contradiction (a proof of False). Formally, you show P → False (by implementing a function of this type), from which it follows that P is uninhabited (there is assuredly no proof).
example: ¬False
example : ¬False: Prop
False :=
λ f: False
f => False.elim: ∀ {C : Prop}, False → C
False.elim f: False
f
The No Contradiction Rule is Valid
This example teaches and reinforces the idea that a proof, np, of a proposition, ¬P, works like a function, namely one that takes a proof of P as an assumed argument, and that in this context constructs and returns a proof of False. The upshot is that if you have in your context both a proof, p, of some proposition, P, and a proof, np, of ¬P, then you have both a function, np: P → false, and an argument, p : P, from which you can derive a proof of False by applying np to p: (np p).
example: ∀ (P : Prop), ¬(P ∧ ¬P)
example (P: Prop
P : Prop: Type
Prop) : ¬(P: Prop
P ∧ ¬P: Prop
P) :=
λ (⟨ p: P
p, np: ¬P
np ⟩) =>
np: ¬P
np p: P
p
Transitivity of Implication is Valid
Prove the transitivity of implication. Note carefully the relationship of this proof to function composition.
example: ∀ (P Q R : Prop), (P → Q) → (Q → R) → P → R
example (P: Prop
P Q: Prop
Q R: Prop
R : Prop: Type
Prop) : (P: Prop
P → Q: Prop
Q) → (Q: Prop
Q → R: Prop
R) → (P: Prop
P → R: Prop
R) :=
fun pq: P → Q
pq qr: Q → R
qr =>
fun p: P
p =>
qr: Q → R
qr (pq: P → Q
pq p: P
p)
-- Here it is in Type, as function composition from HW#3
example: (α β γ : Type) → (α → β) → (β → γ) → α → γ
example (α: Type
α β: Type
β γ: Type
γ : Type: Type 1
Type) : (α: Type
α → β: Type
β) → (β: Type
β → γ: Type
γ) → (α: Type
α → γ: Type
γ) :=
fun ab: α → β
ab bc: β → γ
bc =>
fun a: α
a =>
bc: β → γ
bc (ab: α → β
ab a: α
a)
Or Distributes Over And
This example emphasizes how one reasons when given an assumption that some disjunction is true: it's by case analysis. There are two different ways in which that assumed disjunction could be true, and you need to show that the conclusion follows in either case. So first you assume the first case holds, and show the conclusion follows, then you assume the second case and show that the conclusion still follows. Therefore the conclusion follows in either case. Of course in general you might have to do additional complex reasoning for each case.
example: ∀ (P Q R : Prop), P ∨ Q ∧ R → (P ∨ Q) ∧ (P ∨ R)
example (P: Prop
P Q: Prop
Q R: Prop
R : Prop: Type
Prop) : P: Prop
P ∨ (Q: Prop
Q ∧ R: Prop
R) → (P: Prop
P ∨ Q: Prop
Q) ∧ (P: Prop
P ∨ R: Prop
R)
| Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl p: P
p => ⟨ Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl p: P
p, Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl p: P
p ⟩
| Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr ⟨ q: Q
q, r: R
r ⟩ => ⟨ Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr q: Q
q, Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr r: R
r⟩
At least one of DeMorgan's Laws is Valid
This example reinforces the idea that if you have to return a proof of a negation, ¬X, what you really have to return is a function of type X → False. Note that the return values here are lambda expressions.
example: ∀ (A B : Prop), ¬A ∨ ¬B → ¬(A ∧ B)
example (A: Prop
A B: Prop
B : Prop: Type
Prop) : ¬A: Prop
A ∨ ¬B: Prop
B → ¬(A: Prop
A ∧ B: Prop
B)
| Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl na: ¬A
na =>
λ ⟨ a: A
a, _ ⟩ => na: ¬A
na a: A
a
| Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr nb: ¬B
nb =>
λ ⟨ _, b: B
b ⟩ => nb: ¬B
nb b: B
b
Not All Classical Reasoning is Constructively Valid
Now here's a proposition that seems that it ought to be true. It is classically true. You can check it using our validity checker, with A and B as propositional variables. But we struggled to prove it constructively in class, and there's a good reason for that: it's impossible to construct a proof of either ¬A or ¬B from a proof of ¬(A ∧ B). This one of the four DeMorgan's laws is clasically valid but it is not valid in constructive logic. No wonder we had trouble proving it!
example (example: ∀ (A B : Prop), ¬(A ∧ B) → ¬A ∨ ¬BAA: PropB :B: PropProp) : ¬(Prop: TypeA ∧A: PropB) → (¬B: PropA ∨ ¬A: PropB) |B: Propnab =>nab: ¬(A ∧ B)-- nowhere to go, we're stuck
So are we stuck in Lean with a logic that's actually weaker that propositional or first-order predicate logic--capable of proving fewer theorems? What constructive lacks relative to classical logics is the law of the excluded middle an axiom.
The Law of the Excluded Middle as an Independent Axiom
In classical logics, a proposition is either true or false. In constructive logic, a proposition, P, is true if there's a proof of it, or it's false if there's a proof of ¬P, but its validity is indeterminate if there isn't a proof either way. Consequently, the proposition, P ∨ ¬P, is not necessarily valid. We might not have either a proof of P or of ¬P, in which case we can't prove the disjunction.
example : X ∨ ¬X :=-- no proof; not *constructively valid*
A key difference between constructive and classical logic is that the latter takes the law of the excluded middle (em) as an axiom. It lets you to assume that for any proposition, P, P ∨ ¬P is true, and you can therefore do a case analysis, with one case where P is true (and in Lean has a proof) and one case where ¬P is true (and has a proof).
Taking the law of the excluded middle turns constructive logic back into a classical logic. There is no longer a middle, indeterminate, state to consider. For each proposition, there are only two possible states: that it's true or that its negation is true. Acccepting this assumption as an axiom makes it possible once again to prove that our variant of DeMorgan's law is valid. So let's dive in and see how this works.
First, here's the formal statement of the law of the excluded middle. It says that if you give and proposition, P, it'll return a proof/value of P ∨ ¬P.
axiom em (P : Prop): P ∨ ¬ P
The axiom keywork instructs Lean to accept a definition without a proof, or implementation. What we have here is the definition of em (for excluded middle) as being a kind of proof generator: you feed it any proposition, P, and it returns a proof of the proposition, P ∨ ¬P.
Now here's the crucial trick: Once you have such a proof, you can do case analysis on it. In the first case (Or.inl p), you'll have a proof p : P. In the second case, you'll have a proof, np, of ¬P. One has thus excluded the possibility of a "middle" case, where one doesn't have a proof either way.
Now, of course, proving a proposition, X ∨ ¬X, is trivial, no matter what proposition X represents. The validity of X ∨ ¬X follows from a simple application of em to X.
def P : Prop :=emem: ∀ (P : Prop), P ∨ ¬PPP: Prop
So we've now seen that if given any proposition, P, and the axiom of the excluded middle, you can always obtain a proof of P ∨ ¬P for free. The trick now, again, is to do case analysis on that proof, There are just two cases: Or.inl with a proof (p : P) or Or.inr with a proof(np : ¬P). The "middle" case, where we don't don't a proof either way, no longer needs to be considered.
example: ∀ {X : Prop}, X ∨ ¬X
example : X: Prop
X ∨ ¬X: Prop
X := em: ∀ (P : Prop), P ∨ ¬P
em X: Prop
X
Use em For Classical Reasoning
Accepting the law of the excluded middle as an axiom puts us back in classical reasoning space. Using it, you can get yourself proofs of both A ∨ ¬A and B ∨ ¬B, and now, to prove the validity of the DeMorgan's law variant that had us hung up, it's just a matter of showing that the proposition is true in each of the four resulting cases.
HOMEWORK: Complete this proof.
emem: ∀ (P : Prop), P ∨ ¬Pexample (example: ∀ (A B : Prop), ¬(A ∧ B) → ¬A ∨ ¬BAA: PropB :B: PropProp) : ¬(Prop: TypeA ∧A: PropB) -> ¬B: PropA ∨ ¬A: PropB := λB: Propnab => letnab: ¬(A ∧ B)proof_of_aornota :=proof_of_aornota: A ∨ ¬Aemem: ∀ (P : Prop), P ∨ ¬PA letA: Propproof_of_bornotb :=proof_of_bornotb: B ∨ ¬Bemem: ∀ (P : Prop), P ∨ ¬PB matchB: Propproof_of_aornota with |proof_of_aornota: A ∨ ¬AOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ ba => matcha: Aproof_of_bornotb with |proof_of_bornotb: B ∨ ¬BOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ bb =>b: BFalse.elim (False.elim: ∀ {C : Prop}, False → Cnab (nab: ¬(A ∧ B)And.introAnd.intro: ∀ {a b : Prop}, a → b → a ∧ baa: Ab)) |b: BOr.inrOr.inr: ∀ {a b : Prop}, b → a ∨ bnb =>nb: ¬BOr.inrOr.inr: ∀ {a b : Prop}, b → a ∨ bnb |nb: ¬BOr.inrOr.inr: ∀ {a b : Prop}, b → a ∨ bna =>na: ¬AOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ bnana: ¬A
-- import Mathlib.Init.Set
Predicates
You've seen that in predicate logic, a proposition is a declarative statement that asserts that some state of affairs holds in some domain of discourse. For example, the natural number, four, is an even is a proposition. We've seen one way to formalize such a proposition using the mod operator.
4 %4: Nat2 =2: Nat00: Nat
Families of Propositions
Indeed, there is an infinite family of propositions, all just like this on~`e except for the particular number we plug in instead of four. As another example, the natural number, five, is even is also a proposition. And there's one such proposition for each and every natural number.
We can write this family of propositions by abstracting the value, four, to a variable: e.g., the natural number, n, is even, where n can be any natural number. Now we have a predicate. Applying it to a specific number then returns a proposition about that number.
We could say that applying the predicate, the natural number, n, is even, to the specific number, four, returns the proposition, the natural number, four, is even.
In Lean and related logics, we represent a predicate as a function: from one or more parameter values to propositions. Here's our simple example reformulated.
defis_even :is_even: Nat → PropNat →Nat: TypeProp := λProp: Typen =>n: Natn %n: Nat2 =2: Nat00: Natis_evenis_even: Nat → Prop44: Natis_evenis_even: Nat → Prop44: Natis_evenis_even: Nat → Prop55: Nat
You can see that is_even is a predicate by checking its type. Indeed, it's a function from a natural number to a proposition about that number: namely that the given number mod two is zero. The type of our predicate is thus Nat → Prop.
(is_even) -- Nat → Propis_even: Nat → Prop
Applying a Predicate to Arguments Yields a Proposition
Given a predicate we derive a proposition by applying it to one or more arguments of the specified types. The is_even predicate is appliable to a natural number as an argument. Here are two examples applying the is_even predicate.
is_evenis_even: Nat → Prop44: Natis_evenis_even: Nat → Prop55: Nat
Note that Lean reduced n%2 in each case to 0 or 1, leaving us with simpler propositions involving just 0 and 1.
To Satisfy a Predicate
We will say that specific parameter values satisfy a predicate if they yield a proposition that is true. In a sense, a proposition thus specifies a property (such as that of being even) that a value might or might not have. For example, four has the property of being even but five doesn't.
Predicates Specify Properties
In this way a predicate picks out the subset of parameter values with a specified property. As an example, we can make a list of natural numbers from 0 to 5, apply is_even to each, determine which resulting propositions are true, and thus pick out the natural numbers with the property of being even.
is_evenis_even: Nat → Prop0 -- ✓0: Natis_evenis_even: Nat → Prop1 -- ×1: Natis_evenis_even: Nat → Prop2 -- ✓2: Natis_evenis_even: Nat → Prop3 -- ×3: Natis_evenis_even: Nat → Prop4 -- ✓4: Natis_evenis_even: Nat → Prop5 -- ×5: Nat
Indeed, as we'll see in more depth shortly, we can understand the set of all objects having a particular property as those objects that satisfy the predicate that specifies the property. In Lean, we can specify the set of even numbers as follows, and evens here becomes nothing but a shorthand for is_even. We'll see more of this top when we get to lectures on set theory.
defevens :evens: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) trueSet Nat := {Set: ?m.355n | is_even n }n: ?m.371evens 4evens: {x : Sort ?u.537} → {Set : x} → sorryAx (Sort ?u.536) trueevens 5evens: {x : Sort ?u.549} → {Set : x} → sorryAx (Sort ?u.548) true
Predicates of Multiple Arguments
Predicates can take any number of arguments. Here are some examples.
Ordered pairs of numbers and their squares
defsquare_pair :square_pair: Nat × Nat → PropNat ×Nat: TypeNat →Nat: TypeProp | (Prop: Typen1,n1: Natn2) =>n2: Natn2 =n2: Natn1^n1: Nat22: Natsquare_pair (square_pair: Nat × Nat → Prop1,1: Nat1) -- ✓1: Natsquare_pair (square_pair: Nat × Nat → Prop2,2: Nat4) -- ✓4: Natsquare_pair (square_pair: Nat × Nat → Prop3,3: Nat9) -- ✓9: Natsquare_pair (square_pair: Nat × Nat → Prop5,5: Nat20) -- × def20: Natsquare_pairs :square_pairs: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) trueSet (Nat × Nat) := {Set: ?m.877p :p: Nat × NatNat ×Nat: TypeNat | square_pair p }Nat: Typesquare_pairs (3, 9)square_pairs: {x : Sort ?u.1056} → {Set : x} → sorryAx (Sort ?u.1055) truesquare_pairs (3, 10)square_pairs: {x : Sort ?u.1068} → {Set : x} → sorryAx (Sort ?u.1067) true(3,3: Nat9) ∈9: Natsquare_pairssquare_pairs: {x : Sort ?u.1107} → {Set : x} → sorryAx (Type ?u.1079) true
Here it is again but with two arguments rather than one pair. This material is new relative to that presented in class. Take an extra few minutes to study the precise differences in syntax and sense between these two examples. In one, separate arguments are packed into pairs, whereas in the second, they're not. They're disaggregated.
defsquare_pair' :square_pair': Nat → Nat → PropNat →Nat: TypeNat →Nat: TypeProp |Prop: Typen1,n1: Natn2 =>n2: Natn2 =n2: Natn1^n1: Nat22: Natsquare_pair'square_pair': Nat → Nat → Prop11: Nat1 -- ✓1: Natsquare_pair'square_pair': Nat → Nat → Prop22: Nat4 -- ✓4: Natsquare_pair'square_pair': Nat → Nat → Prop33: Nat9 -- ✓9: Natsquare_pair'square_pair': Nat → Nat → Prop55: Nat20 -- × def20: Natsquare_pairs' :square_pairs': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) trueSet (Nat × Nat) := {Set: ?m.1425p :p: Nat × NatNat ×Nat: TypeNat | square_pair' p.1 p.2 }Nat: Typesquare_pairssquare_pairs: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true(3,3: Nat9) ∈9: Natsquare_pairssquare_pairs: {x : Sort ?u.1641} → {Set : x} → sorryAx (Type ?u.1613) true
When we specify multi-argument predicates our practice is to present the arguments one by one in disaggregated form. Among other things we can then more easily partially evaluate the function on any one of its actual parameters.
Pythagorean triples
defpythagorean_triple :pythagorean_triple: Nat → Nat → Nat → PropNat →Nat: TypeNat →Nat: TypeNat →Nat: TypeProp |Prop: Typeh,h: Natx,x: Naty =>y: Nath^h: Nat2 =2: Natx^x: Nat2 +2: Naty^y: Nat22: Natpythagorean_triplepythagorean_triple: Nat → Nat → Nat → Prop55: Nat44: Nat3 def3: Natpy_trips :py_trips: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) trueSet (Nat × Nat × Nat) := {Set: ?m.2035t | t.1^2 = t.2.1^2 + t.2.2^2}t: ?m.2051py_trips (5,4,3)py_trips: {x : Sort ?u.2213} → {Set : x} → sorryAx (Sort ?u.2212) true
Homework
(1) Define a predicate, ev_len_str, expressing the property of a string of being of an even-length.
-- Here defev_len_str :ev_len_str: String → PropString →String: TypeProp |Prop: Types =>s: Strings.s: Stringlength %length: String → Nat2 =2: Nat0 /- (2) Use #check to typecheck an expression for the set of all even length strings. -/ #check {0: Nats :s: StringString | ev_len_str s } -- Here /- (3) Define a predicate, str_eq_len, applicable to any String value, s, and to any Nat value, n, that is satisfied just in those cases where s.length equals n. -/ defString: Typestr_eq_len :str_eq_len: String → Nat → PropString →String: TypeNat →Nat: TypeProp |Prop: Types,s: Stringn =>n: Nats.s: Stringlength =length: String → Natn -- Here /- (4) Define str_eq_lens : set String × Nat, to be the *set* of all ordered pairs, p = ⟨ s, n ⟩, such that n = s.length. -/ -- Here defn: Natstr_eq_lens :str_eq_lens: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) trueSet (String × Nat) := {Set: ?m.2407p | str_eq_len p.1 p.2} /- (5) Use "example" in Lean to state and prove the proposition that ⟨ "I love Logic!", 13 ⟩ ∈ str_eq_lens. -/ -- Herep: ?m.2423example :example: sorryAx Prop true∈str_eq_lens :=str_eq_lens: {x : Sort ?u.2598} → {Set : x} → sorryAx (Type ?u.2585) truerfl /- (6) Use "example" in Lean again to state and prove that ⟨ "I love Logic!", 1 ⟩ ∉ str_eq_lens. That's shorthand notation for ¬("I love Logic!", 1⟩ ∈ str_eq_lens. And you know what that means. -/ -- Hererfl: ∀ {α : Sort ?u.2606} {a : α}, a = aexample :example: ¬sorryAx Prop true∉str_eq_lens := λ (str_eq_lens: {x : Sort ?u.2730} → {Set : x} → sorryAx (Type ?u.2723) truet :t: sorryAx Prop true∈str_eq_lens) =>str_eq_lens: {x : Sort ?u.2752} → {Set : x} → sorryAx (Type ?u.2739) truett: sorryAx Prop true
(7) Write a formal definition, in Lean, of party, as a set of objects of type Person. Make the Person type inhabited by giving it the single constructor, Person.jim. Hi, jim. Optionally use "structure" for this type, even if you don't know how to change the default constructor name, mk, to jim.
-- HereQuantifiers: Universal Generalization (∀)
Quantifiers are part of the syntax of predicate logic. They allow one to assert that every object (∀) of some type has some property, or that there exists (∃) (there is) at least one (some) object of a given type with a specified property. The syntax of such propositions is as follows:
- ∀ (x : T), P x
- ∃ (x : T), P x
The first proposition can be read as asserting that every value x of type T satisfies predicate P. Universal quantification is a generalized form of a logical and operation: it is used to assert that the first value of a type has some property, and so does the second, and so does the third, through all of them.
In this chapter we address the first case: of propositions in the form of universal generalizations (using ∀). We will cover existential quantification in the next chapter.
Introduction Rule (How to Prove ∀ (x : T), P)
So what does a proof of a universal generalization, ∀ (x : T), P x, require? In the logic of Lean, it requires one to show that a proof can be obtained for each proposition, P x, for each possible value, x : T. Being able to construct a proof of P x for any value x : T shows ∀ (x : T), P x. The way we show this in Lean, in turn, is by defining a function that, when given any t : T, returns a proof of P t for that specific t. The existence of such a function demonstrates that we can construct a proof of P x for any x : T, showing that every x : T has property P.
There's another way to say it that you will hear in less formal presentations. To show ∀ x, P x, assume you have an arbitrary value, x, and show that you can prove P x. That will prove that all values of x satisfy P.
TLDR: To prove ∀ (x : T), P x show that there's a proof of P x for every possible value of x. Do this in Lean by defining a function that takes any value, x : T and that returns a proof of P x for each such x value.
Example
Here's a trivial example. We assert that for every natural number value, n, there is a proof of the corresponding proposition, True. In this case the resulting proposition doesn't depend on the value of the argument, n. The proof of the generalization is a function that takes any natural number, n, ignores it, and returns a proof of True.
example : ∀ (example: ∀ {ℕ : Sort u_1}, ℕ → True:ℕ),ℕ: Sort u_1True := funTrue: Prop=>True.introTrue.intro: True
Function Types and ∀ Propositions
We now see that the logical proposition, ∀ (n : Nat), True, is equivalent to the function type, Nat → True. Given any natural number, n, such a function returns a proof of (a value of type) True. We just gave such a function (value/implementation), and *thereby gave a proof of ∀ (n : ℕ), True.
∀ (:Nat),Nat: TypeTrue -- Literally Nat → True!True: Prop
The function arrow, X → Y is indeed just a notation for ∀ (x : X), Y, the special case of a dependent function type where the return type, here Y, doesn't depend on (vary with) the argument value.
Examples
To further illustrate the equivalence of function arrow and this special case of ∀, here we define the natural number squaring function, declaring its type using ∀ rather than →. But then when we #check it's type, Lean reports it as Nat → Nat, using its default notation, →, for this type.
defsquare : ∀ (square: Nat → Nat:Nat),Nat: TypeNat := λNat: Typen =>n: Natn^n: Nat22: Nat(square) -- Nat → Natsquare: Nat → Natsquaresquare: Nat → Nat5 -- 255: Nat
This next example shows that a proof of ∀ (f : False), False is literally a function of type False → False. Given any proof, f, of False, it's ok to "return a value of type False" because there are no cases in which that will ever have to be done.
deffimpf : ∀ (fimpf: False → False:False),False: PropFalse := λFalse: Propf => nomatchf: Falseff: False(fimpf) -- a value/proof of type False → Falsefimpf: False → False
To drive it home, a proof of a universal generalization, ∀ (x : T), P x, is a function that, when given any value, x : T, as an argument, returns a proof (value) of (type) P x. That functions are always total in Lean means that there will then be a proof of P x for every x : T.
variable
(Dog: Type
Dog : Type: Type 1
Type)
(Blue: Dog → Prop
Blue : Dog: Type
Dog → Prop: Type
Prop)
A formal statement in predicate logic that all dogs are blue.
∀ (:Nat),Nat: TypeNatNat: Type∀ (d :d: DogDog),Dog: TypeBlueBlue: Dog → Propdd: Dog
Dependent Function Types
Note that the return type of this function, P x, depends on the particular value, x : T, to which the function might be applied in any given instance. For each value, x, P x is a different proposition. Each value of x thus gives rise to a different type, one for each value of x.
As we've discussed, a proof of ∀ (x : T), P x is formalized as a function, taking any argument, x : T, and returning a formal proof for the proposition, P x, which is to say a value of type P x.
We see now that in Lean ∀ (x : T), P x is a function type; but it's a function type of a very special kind, in that its return type, P x, depends on the value of the given argument, x. We thus say ∀ (x : T), P x is a dependent function type.
An ordinary function in Lean is a special case where the return type is independent of the argument value. For example, the type Bool doesn't depend on argument values. You can see the difference between dependent and ordinary function values in the following examples.
First, here's an ordinary function type, equivalent to Nat → Bool. Note that the return type, Bool, is fixed and does not vary with the value of the argument, n.
∀ (:Nat),Nat: TypeBool -- function type, Nat → BoolBool: Type
For the second example, we'll go back to our favorite simple predicate, evenness, for natural numbers. We will then explain why ∀ (n : Nat), is_even n is not an ordinary, but a dependent, function type.
To start, here's our is_even predicate again. It returns a different proposition (logical type) for each value of n.
defis_even :is_even: Nat → PropNat →Nat: TypeProp := funProp: Typen =>n: Natn %n: Nat2 =2: Nat0 -- Each of these propositions is a different type in Lean0: Natis_evenis_even: Nat → Prop00: Natis_evenis_even: Nat → Prop11: Natis_evenis_even: Nat → Prop22: Natis_evenis_even: Nat → Prop33: Nat
We can now write a dependent function type: for each value of n it promises to return a value of type, is_even n. We won't be able to implement it because it's not true, so there's no proof of it. But that's not the point here. The point is that this function type has a different return type for each argument value.
∀ (n :n: NatNat),Nat: Typeis_evenis_even: Nat → Propnn: Nat
Totality of Functions
The concept of dependent function types is central to Lean and related languages. Dependent types are how the quantifiers of predicate logic are formalized in Lean.
When you put dependent function types together with the fact that functions are total in Lean, you end up with a crucial piece of the Curry Howard bridge between computation and logic. That a proof of ∀ (x : T), P x is a total function, from any value, x, to a proof of P x, is what makes the function a proof that all argument values satisfy the predicate, P.
A Little Bit of Lean: Declaring variables
The variable command in Lean introduces an identifier and its type without assigning a value. This idea should be familiar from your programming in languages like Java. In Java, you can declare the type of a variable without giving it a value, as in the following example:
String s;
In Lean, one can similarly declare variables without initial values. Here's how we'd translate that Java example into Lean.
variable (s: String
s : String: Type
String)
Having declared such a variable, we can then use it in all the ways Lean allows, except of course reducing it to a value, since no value is yet bound to it. Here we use the #check command to see it's type.
s -- Strings: String
You can declare several variables at a time, as parenthesized type declarations following the variable keyword in Lean.
The following example shows multiple variables being declared. The entire example could all be written on one line but we find it much easier to read when each of the declarations is on its own line.
In particular, here we we declare the following variables.
- T is an arbitrary type
- P to be a one-argument predicate on values of type T
- fa is a proof of the dependent type, ∀ (x : T), P x
- t is an arbitrary value of type T.
namespace decls
variable
(T: Type
T : Type: Type 1
Type)
(P: T → Prop
P : T: Type
T → Prop: Type
Prop)
(fa: ∀ (x : T), P x
fa : ∀ (x: T
x : T: Type
T), P: T → Prop
P x: T
x)
(t: T
t : T: Type
T)
Elimination: Using ∀ Proofs: Universal Specialization
Having declared these variables, we now see how to use a proof of a universal generalization, such as fa: you can apply it to any value, t : T to get a proof that t in particular satisfies (has property) P. Applying fa to t yields a proof of P t. Logicians call this rule of inference universal specialization.
-- fa proves that all α satisfy Pfa -- ∀ (x : α), P x -- Therefore *t* in particular satisfies P -- We obtain a proof by simply applying fa to tfa: ∀ (x : T), P xfafa: ∀ (x : T), P xt -- P t end declst: T
Observe that we're doing logical reasoning having only specified the types of all of our variables.
Here's a less symbolic and abstract example. Suppose we know (have a proof) that All dogs are blue. We can call such a proof, all_dogs_blue. Suppose we also know that that Iris is a dog. We can conclude that Iris is blue. We can formalize and analyze such a logical scenario in Lean using variable declarations, as above.
Here's the formal rendition of our blue dog story.
namespace bluedog
variable
(Dog: Type
Dog : Type: Type 1
Type) -- There are dogs
(Iris: Dog
Iris : Dog: Type
Dog) -- Iris is one
(Blue: Dog → Prop
Blue : Dog: Type
Dog → Prop: Type
Prop) -- The property of being blue
(all_dogs_blue: ∀ (d : Dog), Blue d
all_dogs_blue : ∀ (d: Dog
d : Dog: Type
Dog), Blue: Dog → Prop
Blue d: Dog
d) -- Proof all dogs are blue
Having set up the example, we can now perform the operation of universal specialization to show formally that Iris is blue.
all_dogs_blueall_dogs_blue: ∀ (d : Dog), Blue dIris -- universal specialization end bluedogIris: Dog
The blue dog example (if all dogs are blue and Iris is a dog then Iris is blue) illustrates the application of the rule of logical reasoning that the Greek philosopher, Aristotle, called modus ponens. It's oftentaught using an analogous story about Socrates. It goes like this. (1) All people are mortal. (2) Socrates is a person. (3) Thus Socrates is mortal.
The crucial result to understand is the final check, which produces everyone_is_mortal Socrates : Mortal Socrates. In English, this says that everyone_is_mortal Socrates is a proof of the proposition, Mortal Socrates, that Socrates is mortal under the given assumptions.
namespace socrates variable (Person :Person: TypeType) (Type: Type 1Socrates :Socrates: PersonPerson) (Person: TypeMortal :Mortal: Person → PropPerson →Person: TypeProp) (Prop: Typeeveryone_is_mortal : ∀ (everyone_is_mortal: ∀ (p : Person), Mortal pp :p: PersonPerson),Person: TypeMortalMortal: Person → Propp )p: Personeveryone_is_mortaleveryone_is_mortal: ∀ (p : Person), Mortal pSocrates end socratesSocrates: Person
That brings us to the conclusion of this section on universal generalization and specialization. Key things to remember are as follows:
- Universal generalizations are dependent function types
- To prove a universal generalization (introduction rule for ∀ propositions), define such a dependently typed function
- To use a proof of a universal generalization (elimination), apply such a function (proof()) to a specific value (universal specialization)
Quantifiers: Existential Quantification (∃)
We now turn to the second of the two quantifiers in predicate logic: the existential operator, ∃. It is used to write propositions of the form, ∃ (x : T), P x. This proposition is read as asserting that there is some (at least one) value of type, T, that satisfies P. As an example, we repeat our definition of the is_even predicate, and then write a proposition asserts that there is (there exists) some even natural number.
-- Predicate: defines property of *being even* def is_even : Nat → Prop := λ n => n % 2 = 0 -- Proposition: there exists an even number∃ (n :n: NatNat),Nat: Typeis_evenis_even: Nat → Propnn: Nat
Introduction
In the constructive logic of Lean, a proof of a proposition, ∃ (x : T), P x, has two parts. It's a kind of ordered pair. The first element is a specific value, w : T, that satisfies P. The second element is a proof that w satisfies P (a proof of P w). That we have an object w along with a proof of P w shows that there does exist some object with property P (namely w).
This introduction rule in Lean is called Exists.intro. It takes two arguments: (1) a value, w : T, and a proof of P w. Here's a simple example showing that there exists an even number, with 4 as a witness.
example: ∃ n, is_even n
example : exists (n: Nat
n : Nat: Type
Nat), is_even: Nat → Prop
is_even n: Nat
n := Exists.intro: ∀ {α : Type} {p : α → Prop} (w : α), p w → Exists p
Exists.intro 4: Nat
4 rfl: ∀ {α : Type} {a : α}, a = a
rfl
The witness is 4 and the proof (computed by rfl) is a proof of 4 % 2 = 0, which is to say, of 0 = 0. Try 5 instead of 4 to see what happens.
Lean provides ⟨ _, _ ⟩ as a notation for Exists.intro.
example: ∃ n, is_even n
example : exists (n: Nat
n : Nat: Type
Nat), is_even: Nat → Prop
is_even n: Nat
n := ⟨ 4: Nat
4, rfl: ∀ {α : Type} {a : α}, a = a
rfl ⟩
Another example: Suppose we have a proof that Iris is a blue dog. Can we prove that there exists a blue dog?
namespace bluedog
variable
(Dog: Type
Dog : Type: Type 1
Type) -- There are dogs
(Iris: Dog
Iris : Dog: Type
Dog) -- Iris is one
(Blue: Dog → Prop
Blue : Dog: Type
Dog → Prop: Type
Prop) -- The property of being blue
(iris_is_blue: Blue Iris
iris_is_blue : Blue: Dog → Prop
Blue Iris: Dog
Iris) -- Proof that Iris is blue
-- A proof that there exists a blue dog
example: ∀ (Dog : Type) (Iris : Dog) (Blue : Dog → Prop), Blue Iris → ∃ d, Blue d
example : ∃ (d: Dog
d : Dog: Type
Dog), Blue: Dog → Prop
Blue d: Dog
d := ⟨ Iris: Dog
Iris, iris_is_blue: Blue Iris
iris_is_blue ⟩
end bluedog
An Aside on Constructive Logic
The term constructive here means that to prove that something with a particular property exists, you have to actually have such an object (along with a proof). Mathematicians generally do not require constructive proofs. In other words, mathematicians are often happy to show that something must exist even if they can't construct an actual example.
We call proofs of this kind non-constructive. We saw a similar issue arise with proofs of disjunctions. In particular, we saw that a constructive proof of a disjunction, X ∨ ¬X, requires either a proof of X or a proof of ¬X. Accepting the law of the excluded middle as an axiom permits non-constructive reasoning by accepting that X ∨ ¬X is true without the need to construct a proof of either case.
What one gains by accepting non-constructive reasoning is the ability to prove more theorems. For example, we can prove all four of DeMorgan's laws if we accept the law of the excluded middle, but only three of them if not.
So what does a non-constructive proof of existence look like? Here's a good example. Suppose you have an infinite sequence of non-empty sets, *{ s₀, s₁, ...}. Does there exist a set containing one element from each of the sets?
It might seem obvious that there is such a set; and in many cases, such a set can be constructed. For example, suppose we have an infinite sequence of sets of natural numbers (e.g., { {1, 2}, {3, 4, 5}, ... }). The key fact here is that every such set has a smallest value. We can use this fact to define a choice function that, when given any such set, returns its smallest value. We can then use this choice function to define a set containing one element from each of the sets, namely the smallest one.
There is no such choice function for sets of real numbers, however. Certainly not every such set has a smallest value: just consider the set {1, 1/2, 1/4, 1/8, ...}. It does not contain a smallest number, because no matter what non-zero number you pick (say 1/8) you can always divide it by 2 to get an even smaller one. Given such a set there's no choice function that can reliably returns a value from each set.
As it turns out, whether you accept that there exists a set of elements one from each of an infinity of sets, or not, is your decision. If you want to operate assuming that there is such a set, then you accept what mathematicians call the axiom of choice. It's another axiom you can add to the constructive logic of Lean without causing any kind of contradictions to arise.
The axiom of choice is clearly non-constructive: it gives you proofs of the existence of such sets for free. Most working mathematicians today freely accept the axiom of choice, and so they accept non-constructive reasoning.
Is there a downside to such non-constructive reasoning? Constructive mathematicians argue yes, that it leads to the ability to prove highly counter-intuitive results. One of these is called the Banach-Tarski paradox: a proof (using the axiom of choice) that there is a way cut up and reassemble a sphere that doubles its volume! (Wikipedia article here.)[https://en.wikipedia.org/wiki/Banach%E2%80%93Tarski_paradox]
As with excluded middle, you can easily add the axiom of choice to your Lean environment to enable classical (non-constructive) reasoning in Lean. We will not look further into this possibility in this class.
Elimination Rule for ∃
Now suppose you have a proof of a proposition, ∃ (x : T), P x. That is, suppose you have pf : ∃ (x : T), P x. How can you use such a proof?
Here's the key idea: if you know that ∃ (x : T), P x, then you can deduce two facts: (1) there is some object, call it (w : T), for which, (2) there is a proof, pw, that w satisfies P (a proof of P w). The elimination rule gives us these objects to work with.
Recall that the introduction rule takes a specific value, w, and proof, pf : P w, that that value has property P. Elimination destructures such a proof. What is gives you back, however, is not the specific witness used to create the proof, but rather than arbitrary value, w : T, along with a proof of P w. For this reason, you will see that proofs of existence are called information hiding objects. A specific witness is no longer availabe from a proof of existence.
The easiest way to apply elimination is by pattern matching, as in the following example. It shows that if there exists a number that's true and even, then there's a natural number that's even. Note that what matching gives you is not the specific value used to form the proof, but an arbitrary value, w and a proof pf : P w. That is what you have to work with after applying the elimination rule.
Examples
Here's an example. We want to show that if we have a proof, pf, that there's a natural number, n, that satsifies True and is_even, then there's a natural number, f, that satisfies just is_even.
def ex1: (∃ n, True ∧ is_even n) → ∃ f, is_even f
ex1 : (∃ (n: Nat
n : Nat: Type
Nat), True: Prop
True ∧ is_even: Nat → Prop
is_even n: Nat
n) → (∃ (f: Nat
f : Nat: Type
Nat), is_even: Nat → Prop
is_even f: Nat
f)
| ⟨ w: Nat
w, pf_w: True ∧ is_even w
pf_w ⟩ => Exists.intro: ∀ {α : Type} {p : α → Prop} (w : α), p w → Exists p
Exists.intro w: Nat
w pf_w: True ∧ is_even w
pf_w.right: ∀ {a b : Prop}, a ∧ b → b
right
To show this we destructure pf as ⟨ w, pf_w ⟩. This gives us a witness, w : Nat (whose value we do not know), along with a proof, pf_w, that w (whatever value it is) satifies both True and is_even. Surely then w satisfies is_even by itself. That's the insight.
We can thus form the desired proof by applying Exists.intro to w and a proof that w satisfies is_even. Here w is the witness (value unknown) obtained by destructuring the assumed proof of the premise. We know it's and so will be able to use it as a witness in a proof that there is an even number. Now pf_w is then an assumed proof that w satisfies both True and is_even. From this proof we can derive a proof that w satisfies is_even (by and elimination right). To prove there exists an even number, then, we just apply Exists.intro to w and to pf_w.right. (You can use .2 instead of .right in this expression).
In English we might say this. Prove that if there's a number that is True and even then there's a number that's even.
Proof: Assume there's a number that is True and even. We can then deduce that there is number, w, for which there is a proof, pf that w is True and w is even. From that proof, pf, by and elimination right, we can deduce there's a proof, pf_w_even, that w is even. So we now have a witness, w, and a proof that w is even, so we can form a proof that there exists a number that's even: ⟨ w, pf_w_even ⟩.
Worked Exercise
Formalize and prove the proposition that if there's someone everyone loves, then everyone loves someone.
An informal, English language proof is a good way to start.
Proof. Assume there exists someone, let's call them Beau, whom every person, p, loves. What we need to show is that everyone loves someone. To prove this generaliation, we'll assume that p is an arbitrary person and will show that there is someone p loves. But everyone loves beau so, by universal specialization, p loves Beau. Because p is arbitrary, this shows (by forall introduction) that every person loves someone (namely beau).
namespace cs2120f23
variable
(Person: Type
Person : Type: Type 1
Type)
(Loves: Person → Person → Prop
Loves : Person: Type
Person → Person: Type
Person → Prop: Type
Prop)
example: ∀ (Person : Type) (Loves : Person → Person → Prop),
(∃ beau, ∀ (p : Person), Loves p beau) → ∀ (p : Person), ∃ q, Loves p q
example :
-- if there's someone everyone loves
(∃ (beau: Person
beau : Person: Type
Person), ∀ (p: Person
p : Person: Type
Person), Loves: Person → Person → Prop
Loves p: Person
p beau: Person
beau) →
-- then everyone loves someone
(∀ (p: Person
p : Person: Type
Person), ∃ (q: Person
q : Person: Type
Person), Loves: Person → Person → Prop
Loves p: Person
p q: Person
q)
-- call the person everyone loves beau
-- call the proof everyone loves beau everyone_loves_beau
| ⟨ beau: Person
beau, everyone_loves_beau: ∀ (p : Person), Loves p beau
everyone_loves_beau ⟩ =>
-- prove everyone loves someone by ∀ introduction
-- assume you're given an arbitrary person, p
fun (p: Person
p : Person: Type
Person) =>
-- then show that there exists someone p loves
-- with beau as a witness
-- and a proof p loves beau (by universal specialization)
⟨beau: Person
beau, (everyone_loves_beau: ∀ (p : Person), Loves p beau
everyone_loves_beau p: Person
p)⟩
end cs2120f23
Here's the same logical story presented in a more abstract form, using T instead of Person and R : T → T → Prop to represent the binary relation (previously Loves) on objects of type T.
variable
(T: Type
T : Type: Type 1
Type)
(R: T → T → Prop
R : T: Type
T → T: Type
T → Prop: Type
Prop)
-- Here
example: ∀ (T : Type) (R : T → T → Prop), (∃ p, ∀ (t : T), R t p) → ∀ (p : T), ∃ t, R p t
example : (∃ (p: T
p : T: Type
T), (∀ (t: T
t : T: Type
T), R: T → T → Prop
R t: T
t p: T
p)) →
(∀ (p: T
p : T: Type
T), (∃ (t: T
t : T: Type
T), R: T → T → Prop
R p: T
p t: T
t))
| ⟨ w: T
w, pf_w: ∀ (t : T), R t w
pf_w ⟩ => (fun (p: T
p : T: Type
T) => ⟨ w: T
w, pf_w: ∀ (t : T), R t w
pf_w p: T
p ⟩)
In mathematical English: Given a binary relation, R, on objects of type T, if there's some p such that forall t, R t p (every t is related to p by R), then for every p there is some t such that R p t (every p is related to some t). In particular, every p is related to w, the person everyone loves. So everyone loves someone.
Homework
Forthcoming.
-- import Mathlib.Data.Set.Basic
-- import Mathlib.Logic.Relation
Set Theory
A set is intuitively understood as a collection of objects. Such a collection can be finite or infinite. For example, the set of natural numbers less than five is finite but the set of all natural numbers is infinite.
Like Boolean algebra or arithmetic, set theory has both objects (sets, in the case) and operations on them. Your main aims in this chapter are to (1) understand the language of sets and operations on them in the abstract, (2) understand how sets and set operations are represented in predicate logic (and in Lean), (3) understand how to prove propositions about sets by proving their underlying logical propositions.
The first section of introduces sets and how they are defined as predicates in Lean. The second presents the major operations of set theory, and along the way explains the logic underpinning each set theory operation.
We now turn to the objects of set theory, namely sets. One we understand how sets are defined by predicates, we will turn to the logical definitions of the operations of set theory.
Sets
In Lean, a set is represented by a predicate, one that is made true by every member of the set, and not by any other values. We call a predicate used to define a set a membership predicate.
You can see the actual definition of Set in Lean by going to its definition. Right click on Set and select go to definition.
What you'll find is def Set (α : Type u) := α → Prop. In other words, the type, Set α, in Lean, really is just the type, α → Prop. A set truly is represented directly by a predicate in this sense.
Membership Predicates
Let's start by building on our understanding of predicates. Here are two predicates on natural numbers. The first is true of even numbers. The second is true of any number that is small, where that is defined as the number being equal to 0, or being equal to 1 or, ..., or being equal to 4. The first predicate can be understood as specifying the set of even numbers; the second predicate, a set of small numbers.
def ev := λ n : Nat => n % 2 = 0
def small := λ n : Nat => n = 0 ∨ n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 4
Self test: What proposition is specified by the expression, small 1? You should be able to answer this question without seeing the following answer.
Answer: Plug in a 1 for each n in the definition of small to get the answer. There are 5 places where the substitution has to be made. Lean can tell you the answer. Study it until you see that this predicate is true of all and only the numbers from 0 to 4 (inclusive).
smallsmall: Nat → Prop11: Nat
The result is 1 = 0 ∨ 1 = 1 ∨ 1 = 2 ∨ 1 = 3 ∨ 1 = 4. This proposition is true, of course, because 1 = 1. So 1 is proved to be a member of the set that the predicate specifies. Similarly applying the predicate to 3 or 4 will yield true propositions; but that doesn't work for 5, so 5 is not in the set that this predicate specifies.
To formally prove that 1 is in the set, you prove the underlying logical proposition, 1 = 0 ∨ 1 = 1 ∨ 1 = 2 ∨ 1 = 3 ∨ 1 = 4. A proof of set membership thus reduces to a proof of an ordinary logical proposition, in this case a disjunction. Again an insight to be taken from this chapter is that set theory in Lean reduces to correspondinglogic you already understand and know how to deal with.
As a reminder, let's prove 1 = 0 ∨ 1 = 1 ∨ 1 = 2 ∨ 1 = 3 ∨ 1 = 4.
First, recall that ∨ is is right associative, so what we need to prove is (1 = 0) ∨ (1 = 1 ∨ 1 = 2 ∨ 1 = 3 ∨ 1 = 4). It takes just a little analysis to see that there is no proof of the left side, 1 = 0, but there is a proof of the right side. The right side is true because 1 = 1. Our proof is thus by or introduction on the right applied to a proof of the right side, which we can now slightly rewrite as (1 = 1) ∨ (1 = 2 ∨ 1 = 3 ∨ 1 = 4).
Be sure to see that using right introduction discards the left side of the original proposition and requires only a proof of the right. A proof of it, in turn, is by or introduction on the left applied to a proof of 1 = 1. That proof is by the reflexive property of equality (it's always true that anything equals itself). This idea is expressed in Lean using rfl.
Exercise: Give a formal proof that 1 satisfies the small predicate. We advise you to use top-down, type-guided structured proof development to complete this simple proof. We give you the or introduction on the right to start.
example :example: small 1smallsmall: Nat → Prop1 := (1: NatOr.inr (Or.inr: ∀ {a b : Prop}, b → a ∨ b))
Set Theory Notation
In the language of set theory, there are two especially common notations for represeting sets. They are display and set comprehension notation.
Display Notation
To represent a finite set of objects in mathematical writing, you can give a comma-separated list of members between curly braces. The set of small numbers (0 to 4) can be represented in this way as { 0, 1, 2, 3, 4 }. Sometimes we will want to give a set a name, as in, let s = { 0, 1, 2, 3, 4 }, or let s be the set, { 0, 1, 2, 3, 4 }.
Lean supports display notation as a set theory notation. One is still just definining a membership predicate, but it looks like the math you'll see in innumerable books and articles.
The corresponding predicate in this case, computed by Lean, is λ n => n = 0 ∨ n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 4. In the following example, Lean doesn't infer that the set type is Set Nat, so we have to tell it so explicitly.
defs1 :s1: {x : Sort u_1} → {Set : x} → sorryAx (Type u_2) trueSet Nat :=Set: ?m.204{ 0, 1, 2, 3, 4 }{: sorryAx (Type u_2) trues1 -- the predicate that represents this sets1: {x : Sort u_1} → {Set : x} → sorryAx (Type u_2) true
Set Comprehension Notation
Sets can also be specified using what is called set comprehension notation. Here's an example using it to specify the same small set.
def s2: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
s2 : Set: ?m.275
Set Nat := { n: Nat
n : Nat: Type
Nat | n = 0 ∨ n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 4 }
We pronounce the expression (to the right of the := of course) as *the set of values, n, of type Nat, such that n = 0 ∨ n = 1 ∨ n = 2 ∨ n = 3 ∨ n = 4. The curly braces indicate that we're defining a set. The n : Nat specifies the set of set members. The vertical bar is read such that, or satisfying the constraint that. And the membership predicate is then written out.
You can check that this set, s2, has the same membership predicate as s1.
s2s2: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
Example: Assume there's a type of objects call Ball and a predicate, Striped, on balls. Use set comprehension notation to specify the set of striped balls. Answer: { b : Ball | Striped b }. Read this expression in English as the set of all balls, b, such that b is striped, or more concisely and naturally simply as the set of all striped balls.
Aside On Homogeneity
The preceding example involved a set of natural numbers. In Lean, such a set, being defined by a predicate on the natural numbers, cannot contain elements that are not of the natural number type. Sets in Lean are thus said to be homogeneous. All elements are of the same type. This makes sense, as sets are defined by predicates that take arguments of fixed types.
A heterogeneous set, by contrast, can have members of different types. Python supports heterogeneous sets. You can have a set containing a number, a string, and a person. The track in Python is that all objects actually have the same static type, which is Object. In the end, even in Python, sets are homogeneous in this sense.
In Lean, and in ordinary mathematics as well, sets are most often assumed to be homogenous. In mathematical communication, one will often hear such phrases as, Let T denote the set of natural numbers less than 5. Notice that the element type is made clear.
In support of all of this, Set, in Lean, is a type builder polymorphic in the element type. The type of a set of natural numbers is Set Nat, for example, while the type of a set of strings is Set String.
The homogeneity of sets, in turn allows sets to be represented by membership predicates. We represent a set of objects of some type T as a predicate, P : T → Prop, such that P is true (has a proof) for every value in the set and for no others. All of the elements of such a set are thus necessarily of the same type: in this case, T.
The following example shows that, in Lean, the even and small predicates we've already defined can be assigned to variables of type Set Nat. It type-checks! Sets truly are specified by and equated with their membership predicates in Lean.
defev_set' :ev_set': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true:=ev -- ev is a predicate defev: Nat → Propsmall_set' :small_set': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true:=small -- small is toosmall: Nat → Prop
It'd be unusual in mathematical writing however to define sets in this style. Better would be to use either display or set comprehension notation. Here are stylistically improved definitions of our sets of even and small natural numbers. We will use these definitions in running examples in the rest of this chapter.
def ev_set: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
ev_set : Set: ?m.805
Set Nat := { n: Nat
n : Nat: Type
Nat | ev n }
def small_set: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
small_set : Set: ?m.981
Set Nat := { n: ?m.997
n | small n }
The take-away is that, no matter one's choice of notation, sets are truly represented in Lean by logical predicates. The great news is that you already understand the logic so learning set theory is largely reduced to learning the set algebraic concepts (the objects and operations of set theory) and in particular how each concept reduces to underlying logic.
Set Theory Operations
We now turn to the operations and corresponding notations of set theory. Along the way we'll introduce two special sets: the universal set of objects of a given type, and the empty set of objects of a given type. A universal set contains every value of its member type. The empty set contains no values of its member type.
Membership
We've already seen that we can think of a predicate as defining a set, and that a value is a member of a set if and only if it satisfies the membership predicate.
That said, set theory comes with its own abstractions and notations. For example, we usually think of a set as a collection of objects, even when the set is specified by a logical membership predicate. Similarly set theory gives us notation for special sets and all of the operations of set theory.
As an example, the proposition that 1 is a member of small_set would be written as small_set 1 if we're thinking logically; but in set theory we'd write this as 1 ∈ small_set. We would pronounce this proposition as 1 is a member of small_set.
From now on you should try to interpret such an expression in two ways. At the abstract level of set theory, it asserts that 1 is a member of the collection of elements making up small_set. At a concrete, logical, level, it means that small_set 1, the logical proposition that 1 satisfies the small_set predicate, is true, and that you can construct a proof of that.
The very same proof proves 1 ∈ small_set. All these notations mean the same thing, but set theory notation encourages us to think more abstractly: in terms of sets (collections), not predicates, per se.
Nevertheless, to construct proofs in set theory in Lean, you must understand how the objects and operations in set theory are defined in terms of, and reduce, to propositions in pure logic. What you will have to prove are the underlying logical propositions.
Here, for example, we state a proposition using set theory notation, but the proof is of the underlying or proposition.
#check1 ∈1: Natsmall_set -- membership proposition in set theorysmall_set: {x : Sort ?u.1172} → {Set : x} → sorryAx (Type ?u.1158) true1 ∈1: Natsmall_set -- this proposition in predicate logicsmall_set: {x : Sort ?u.1204} → {Set : x} → sorryAx (Type ?u.1190) true:1 ∈1: Natsmall_set :=small_set: {x : Sort ?u.1245} → {Set : x} → sorryAx (Type ?u.1225) trueOr.inr (Or.inr: ∀ {a b : Prop}, b → a ∨ bOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ brfl) -- a proof of itrfl: ∀ {α : Sort ?u.1268} {a : α}, a = a
The lesson is that when you look at an expression in set theory you really must understand its underlying logical meaning, for it's the underlying logical proposition that you'll need to prove.
So we're now in a position to see the formal definition of the membership operation on sets in Lean. In the Lean libraries, it is def Mem (a : α) (s : Set α) : Prop := s a, where α is a type. The notation ∈ reduces to corresponding logic. More conretely, the set theory proposition a ∈ s reduces to applying the set, s, viewed as a membership predicate, to the argument, a (thus the expression, s a) to yield a proposition, (s a), that is true if and only if a is in s.
Exercises.
(1) We expect that by now you can construct a proof of a disjunction with several disjunctions. But practice is still great and necessary. Try erasing the given answer and re-creating it on your own. By erase we mean to replace the answer with _. Then use top-down, type-guided refinement to derive a complete proof in place of the _.
3 ∈3: Natsmall_setsmall_set: {x : Sort ?u.1398} → {Set : x} → sorryAx (Type ?u.1384) true:3 ∈3: Natsmall_set :=small_set: {x : Sort ?u.1439} → {Set : x} → sorryAx (Type ?u.1419) trueOr.inr (Or.inr: ∀ {a b : Prop}, b → a ∨ bOr.inr (Or.inr: ∀ {a b : Prop}, b → a ∨ bOr.inr (Or.inr: ∀ {a b : Prop}, b → a ∨ bOr.inlOr.inl: ∀ {a b : Prop}, a → a ∨ brfl)))rfl: ∀ {α : Sort ?u.1466} {a : α}, a = a
Take-Away
A take-away is that the set theory expression, x ∈ X, simply means, that x satisfies the membership predicate that defines the set X. To prove x ∈ X, substitute x for the formal parameter in the membership predicate (apply the predicate to x) and prove the resulting proposition.
Two Special Sets
With membership notation under our belts, we can now better present the concepts and notations of the universal and the empty set of elements of a given type.
Universal set
The universal set of a values of a given type is the set of all values of that type. The membership predicate for the universal set is thus true for every element of the set. True is the (degenerate, parameterless) predicate that satisfies this condition. It is true for any value, so every value is in a set with True as its membership predicate.
To be precise, the membership predicate for the universal set of objects of any type T, is λ (a : T) => True. When it is applied to any value, t, of type T, the result is just the proposition, True, for which we always have the proof, True.intro.
In Lean, the universal set of objects of a given type is written as univ. The definition of univ is in Lean's Set namespace, so you can use univ either by first opening the Set namespace, or by writing Set.univ.
open-- fun _a => True0 -- True0: ?m.1587123456 -- True123456: ?m.1591
Empty set
The empty set of values of a given type, usually denoted as ∅, is the set containing no values of that (or any) type. It's membership predicate is thus false for every value of the type. No value is a member. Formally, the membership predicate for an empty set of values of type T is λ (t : T) => False.
Again we emphasize that set theory in Lean is built on and corresponds directly with the logic you've been learning all along. We've now seen that (1) sets are specified by membership predicates; (2) the universal set is specified by the predicate that is true for any value; (3) the empty set is specified by the predicate that is false for any value; (4) the ∈ operation builds the proposition that a given value satisfies the membership predicate of a given set; (5) proving propositions in set theory reduces to proving corresponding underlying logical propositions.
At an abstract level, Set theory, like arithmetic, is a mathematical system involving objects and operations on these objects. In arithmetic, the objects are numbers and the operations are addition, multiplication, etc. In Boolean algebra, the objects are true and false and operations include and, or, and not. In set theory, the objects are sets and the operations include set membership (∈), intersection (∩), union (∪), difference (), complement (ᶜ) and more. We now turn to operations on sets beyond mere membership.
Intersection
Given a type, T, and two sets, s1 and s2 of T-valued elements (members), the intersection of s1 and s2 is the set the members of which are those values that are in both s1 and s2. The intersection of s1 and s2 is written mathematically as s1 ∩ s2.
The intersection operation is defined in Lean as inter (s₁ s₂ : Set α) : Set α := {a | a ∈ s₁ ∧ a ∈ s₂}. Given two sets of alpha values, the result is the set of values, a, that satisfy both conditions: a ∈ s₁ ∧ a ∈ s₂. Set intersection (∩) is defined by predicate conjunction (∧).
Intersection of sets corresponds to logical conjunction (using and) of the respective set membership predicates. The similarity in notations reflects this fact, with ∩ in the language of set theory reducing to ∧ in the language of predicate logic. The following Lean codeillustrate the point.
-- fun s₁ s₂ a => s₁ a ∧ s₂ a variable (α : Type) (s t : Set α)s ∩ t -- the intersection of sets is a sets: sorryAx (Sort ?u.1620) trues ∩ t -- its membership predicate is formed using ∧s: sorryAx (Sort ?u.1655) true
As another example, the intersection of our even (ev) and small sets, corresponding to the conjunction of their membership predicates, contains only the elements 0, 2, and 4, as these are the only values that satisfy both the ev and small predicates.
∩ small_set -- intersection!(0 ∈0: Nat) -- membership proposition
As an example, let's prove 6 ∈ even_and_small_set. We'll first look at the logical proposition corresponding to the proposition in set theory assertion, then we'll try to prove tha underlying logical proposition.
6 ∈6: Nat-- to prove: 0 = 0 ∧ (6 = 0 ∨ 6 = 1 ∨ 6 = 2 ∨ 6 = 3 ∨ 6 = 4) example::= _
The proposition to be proved is a conjunction. A proof of it will have to use And.intro applied to proofs of the left and right conjuncts. The notation for this is ⟨ _, _ ⟩, where the holes are filled in with the respective proofs. We can make a first step a top-down, type-guided proof by just applying this proof constructor, leaving the proofs to be filled in later. The Lean type system will tell us exactly what propositions then remain to be proved.
example::= ⟨ _, _ ⟩
On the left, we need a proof of 6 ∈ ev_set. This can also be written as ev_set 6, treating the set as a predicate. This expression then reduces to 6 % 2 = 0, and further to 0 = 0. That's what we need a proof of on the left, and rfl will construct it for us.
example::= ⟨ rfl, _ ⟩
Finally, on the right we need a proof of 6 ∈ small_set. But ah ha! That's not true. We can't construct a proof of it, and so we're stuck, with no way to finish our proof. Why? The proposition is false!
Exercise: Prove that 6 ∉ small_set. Here you have to recall that 6 ∉ small_set means ¬(6 ∈ small_set), and that in turn means that a proof (6 ∈ small_set) leads to a contradiction and so cannot exist. That is, that 6 ∈ small_set → False.
This is again a proof by negation. We'll assume that we have a proof of the hypothesis of the implication (h : 6 ∈ even_and_small_set), and from that we will derive a proof of False (by case analysis on a proof of an impossibility using nomatch) and we'll be done.
example ::= fun (h : 6 ∈ even_and_small_set) => nomatch h
A Remark on Set Theory Notation
One place where meanings of predicates and sets differ in Lean is in the availability of certain notations. Lean gives us notations appropriate to treating even_and_small as just a predicate, not a set, so set notation operations are not provided in this case. For example, the is member of set predicate, ∈, can't be used to with just a predicate. It's meant for cases where the predicate is meant to represent a mathematical set. Set operations and notations, such as ∈, are provided to support the mathematical concepts involved in set theory.
Union
Given two sets, s and t, the union of the sets, denoted as s ∪ t, is understood as the collection of values that are in s or in t. The membership predicate of s ∪ t is thus *union (s₁ s₂ : Set α) : Set α := {a | a ∈ s₁ ∨ a ∈ s₂}. As an example, we now define even_or_small_set as the union of the even_set and small_set.
-- fun {α} s₁ s₂ a => s₁ a ∨ s₂ a∪ small_set
Now suppose we want to prove that 3 ∈ even_or_small_set. What we have to do is prove the underlying logical proposition. We can confirm what logical proposition we need to prove using reduce.
3 ∈3: Nat
Exercises. Give proofs as indicated. Remember to analyze the set theoretic notations to determine the logical form of the underlying membership proposition that you have to prove is satisfied by a given value.
example ::= Or.inr _ example ::= _example :example: ¬sorryAx Prop true7 ∉7: Natev_set ∪ small_set := _ev_set: {x : Sort ?u.2850} → {Set : x} → sorryAx (Type ?u.2836) trueexample :example: sorryAx Prop true7 ∈7: Natev_set :=ev_set: {x : Sort ?u.2923} → {Set : x} → sorryAx (Type ?u.2903) true-- stuck example : 7 ∉ ev_set := λ h =>
Set Complement
Given a set s of elements of type α, the complement of s, denoted sᶜ, is the set of all elements of type α that are not in s. Thus compl (s : Set α) : Set α := {a | a ∉ s}.
So whereas intersection reduces to the conjunction of membership predicates, and union reduces to the disjunction of membership predicates, the complement operation reduces to the negation of membership predicates.
sᶜ -- fun x => x ∈ s → False means fun x => x ∉ s -- fun x => x ∈ s → False variable (s: sorryAx (Sort ?u.3022) trues :s: sorryAx (Sort ?u.3084) true)sᶜ -- Standard notation for complement of set ss: sorryAx (Sort ?u.3104) true
Exercises:
(1) State and prove the proposition that 5 ∈ smallᶜ. Hint: You have to prove the corresponding negation: ¬5 ∈ small_set.
example: sorryAx Prop true
example : 5: Nat
5 ∈ small_set: {x : Sort ?u.3205} → {Set : x} → sorryAx (Type ?u.3185) true
small_setᶜ := _
Set Difference
-- fun s t a => s a ∧ (a ∈ t → False) -- fun s t a => a ∈ s ∧ a ∉ t (better abstracted expression of same idea) example : 6 ∈ ev_set \ small_set := ⟨ rfl, λ h => nomatch h ⟩6 ∈6: Natev_set \ small_setev_set: {x : Sort ?u.3419} → {Set : x} → sorryAx (Type ?u.3405) true
Subset
-- fun {α} s₁ s₂ => ∀ ⦃a : α⦄, a ∈ s₁ → s₂ a
Powerset
-- fun {α} s t => ∀ ⦃a : α⦄, a ∈ t → s a
Set Theory and Logical Underpinnings
| Set Theory Concept | Set Theory Definition | Constructive Logic Reduction (Lean) |
|---|---|---|
| set α | axioms of set theory | predicate (α → Prop in Lean) |
| s ∩ t | { a | a ∈ s ∧ a ∈ t } | λ a => s a ∧ t a |
| s ∪ t | { a | a ∈ s ∨ a ∈ t } | λ a => s a ∨ t a |
| sᶜ | { a | a ∉ s } | λ a => s a → False |
| s \ t | { a | a ∈ s ∧ a ∉ t } | λ a => s a ∧ (t a → False) |
| s ⊆ t | ∀ a, a ∈ s → a ∈ t ... | λ a => s a → t a ... |
| s ⊊ t | ... ∧ ∃ w, w ∈ t ∧ w ∉ s | ... ∧ ∃ w, (t w) ∧ (s w → False) |
| 𝒫 s | { t | t ⊆ s } | λ t => ∀ ⦃a : ℕ⦄, t a → s a |
In set theory, you have an example of one mathematical abstraction with its own objects (sets) and operations (as in the table). Here we have even more: how set theory language reduces to the language of predicate logic in Lean. You should know not only the meanings of the abstract operations, such as intersection, but how each is defined in terms of predicate logic. You will have to translate back and forth, because you have to understand set theory propositions at the logical level level to see how to construct proofs of them.
Homework #2
Procedure
Here's how to do this homework.
Collaboration
The collaboration rule for this homework is that you may not collaborate. You can ask friends and colleagues to help you understand material in the class notes, but you may not discuss any aspect of this homework itself with anyone other than one of the instructors or TAs.
Purpose
It is vitally important for your ability to do well for the rest of the semester that you now master all of the material we've covered so far in class. The purpose of this homework assignment is to guide you to such mastery.
Questions?
We will provide a means for you to ask questions in case you get terribly stuck. Before you ask, be sure you've read, worked in VSCode through, and done your best to understand all of the materials covered so far in class. You may, and we encourage you to, work with your in-class friends to this end. We'll announce a question-asking forum shortly.
What To Do
Do a git pull upstream main to pull this homework file into your Instructor/Homework directory. Copy it to your Students directory. Complete the work by editing that file. Save it to your Local downloads folder. Upload the completed homework file to Canvas.
Due Date
This assignment is due before class, by 3PM, next Tuesday, September, 5.
Problem 1: A type question
The String.length function takes any string as an argument and returns the natural number that is length of the given string. What is the type of the String.length function?
Answer here:
2: Define a Boolean operation
The implies function from Boolean algebra takes two Boolean values as its arguments and returns a Boolean value as a result. It's one of the standard Boolean operations, along with and, or, etc. It behaves as follows: If the first argument is true and the second is false, then it returns false and otherwise it returns true.
Define the implies function here, calling it imp. Hint: Review the notes to see how we define several similar Boolean operators, such as xor and nor.
-- Write your code here
Problem 3: Prove correctness by exhaustive testing
Prove that your implementation of imp is corret by writing test cases, using #eval, for all possible combinations of input arguments. Document the corret (expected) answer for each case in a comment at the end of each #eval line, as we've done in class.
-- Write your answers here: #eval-- etc
4. Glue together two compatible functions
Write a function, glue_funs', that takes three arguments and returns a Boolean result. The first argument is any function, g, that takes a Nat and returns a Bool. The second argument, is any function, f, that takes a String and returns a Nat. The third argument is a String, s. The glue_funs' function computes its result value by applying g to the result of applying f to s.
As an example, suppose f is the String.length function. It takes a String and returns the Nat that is its length. Now suppose that g is the isEven function, as we define below, that takes a natural number and returns true if it's even and false otherwise. Finally, suppose that s is the string, "Hello." Then we expect the application, (glue_funs' g f s) to return false, because (1) applying f to s returns 5, then (2) applying g to 5 returns false (because it's not even), and that is the final Bool result that glue_funs' is meant to return.
Define (implement) your glue_funs' function below, then test it using String.length as f, the isEven function (we define it for you) as g, and at least the two String values, "Hello" and "Hello!" as test inputs for s.
Hint: Be sure you really understand how we defined the apply2 functions in class. See the class notes. That function is a special case where we apply a function f to the result of applying that same function, f, to an argument. The function you're asked to implement here "glues" together different functions, g and f. First we sure that you've written the type of glue_funs' correctly, then the implementation code should be straightforward. Note that the g argument comes first, then the f argument, and finally s.
-- Here's an implementation of isEven def isEven : Nat → Bool | n => if (n%2 = 0) then true else false(isEven) -- Nat → BoolisEven: Nat → BoolisEvenisEven: Nat → Bool2 -- expect true2: NatisEvenisEven: Nat → Bool3 -- expect false3: Nat
Define your function here. When you've got it right, the following test cases should pass.
-- Now complete the implementation of glue_funs' defglue_funs' :glue_funs': (x : ?m.5416) → ?m.5417 x| _ => _ #evalisEven String.length "Hello" -- false #evalisEven String.length "Hello": ?m.5432isEven String.length "Hello!" -- trueisEven String.length "Hello!": ?m.5451
5. Generalize to arbitrary compatible function types
Note that glue_funs' works only for functions of types (Nat → Bool) and (String → Nat). It works because the output type of any (String → Nat) function is the input type of any (Nat → Bool) function. So in a sense we can always glue together any two such functions.
It should be evident at this point that this concept generalizes. Suppose we have any three types, α, β, and γ; that we have any two functions, g : β → γ, and f : α → β; and that we have any value, a : α. Then we can always compute and return g (f a). We can say that our new function returns the value gotten by applying g after f (to a).
Your task now is to show that you have mastered and that you can apply the concepts we've covered to far in class (and in the class notes) by now implementing a polymorphic version of glue_funs', with implicit type arguments. Call your new function glue_funs. When you succeed, the test cases below should pass.
Lean hint: you can declare multiple arguments of the same type within a single set of parentheses. For example, if you want to declare x, y, and z to be of type Nat, you could write (x y z : Nat). It's not necessary to do this but it yields cleaner "code."
Another hint: You might find it useful to write your glue_funs definition first without implicit type arguments then make them implicit. That's up to you. If you do that, you'll have to give the type argument values explicitly in the test cases below. They are currently written assuming that the values of the type arguments are implicit and inferred.
-- Implement glue_funs here defglue_funs :glue_funs: (x : ?m.5481) → ?m.5482 x| _ => _ -- test cases #evalisEven String.length "Hello" -- false #evalisEven String.length "Hello": ?m.5497isEven String.length "Hello!" -- trueisEven String.length "Hello!": ?m.5516
6. Show that apply2 is a special case
Write a test case for glue_funs using the double and square functions from the class notes as function arguments. (You can copy those function definitions into this file.) What do you expect the result to be for (glue_funs double square 5)? Do you expect to get the same answer for (glue_funs square double 5)? In other words is applying double after square the same as applying square after double?
-- Copy the double and square functions here -- Write your tests here; include expected results #eval#eval
Homework #3
The purpose of this homework is to strengthen your understanding of function composition and of enumerated and product data types.
Overview and Rules
The collaboration rule for this homework is that you may not collaborate. You can ask friends and colleagues to help you understand material in the class notes, but you may not discuss any aspect of this homework itself with anyone other than one of the instructors or TAs. Why? Because you need to learn this material to pass the exam to come.
Problem #1
Define a function of the following polymorphic type: {α β γ : Type} → (β → γ) → (α → β) → (α → γ). Call it funkom. After the implicit type arguments it should take two function arguments and return a function as a result.
-- Answer below
Problem #2
Define a function of the following polymorphic type: {α β : Type} → (a : α) → (b : β) → α × β. Call it mkop.
-- Answer below
Problem #3
Define a function of the following polymorphic type: {α β : Type} → α × β → α. Call it op_left.
-- Answer below
Problem #4
Define a function of the following polymorphic type: {α β : Type} → α × β → β. Call it op_right.
-- Answer below
Problem #5
Define a data type called Day, the values of which are the names of the seven days of the week: sunday, monday, etc.
Some days are work days and some days are play days. Define a data type, kind, with two values, work and play.
Now define a function, day2kind, that takes a day as an argument and returns the kind of day it is as a result. Specify day2kind so that weekdays (monday through friday) are work days and weekend days are play days.
Next, define a data type, reward, with two values, money and health.
Now define a function, kind2reward, from kind to reward where reward work is money and reward play is health.
Finally, use your funkom function to produce a new function that takes a day and returns the corresponding reward. Call it day2reward.
Include test cases using #reduce to show that the reward from each weekday is money and the reward from a weekend day is health.
Problem #6
A.
Consider the outputs of the following #check commands.
Nat ×Nat: TypeNat ×Nat: TypeNatNat: TypeNat × (Nat: TypeNat ×Nat: TypeNat)Nat: Type(Nat ×Nat: TypeNat) ×Nat: TypeNatNat: Type
Is × left associative or right associative? Briefly explain how you reached your answer.
Answer here:
B.
Define a function, triple, of the following type: { α β γ : Type } → α → β → γ → (α × β × γ)
-- Here:
C.
Define three functions, call them first, second, and third, each of which takes any such triple as an argument and that returns, respectively, its first, second, or third elements.
-- Here:
D.
Write three test cases using #eval to show that when you apply each of these "elimination" functions to a triple (that you can make up) it returns the correct element of that triple.
-- Here:
E.
Use #check to check the type of a term. that you make up, of type (Nat × String) × Bool. The challenge here is to write a term of that type.
Homework #4
The PURPOSE of this homework is to greatly strengthen your understanding of the construction and use of the data types we've introduced to far, especially the sum and product types. You will be asked to solve problems that in some cases will require a bit of programming creativity, requiring you to to put together several of the ideas we've covered so far.
READ THIS: It is vitally important that you learn how to solve these problems on your own. You will have to be able to do this to do well on the first exam, a month or so away. Therefore, the collaboration rule for this homework is that you may not collaborate. You can ask friends and colleagues to help you understand the class material, but you may not discuss this homework itself with anyone other than one of the instructors or TAs.
#1: Read All of the Class Notes
You won't be graded on this part of the assignment, but it is nevertheless serious and required work on your part. Read and genuinely understand all the class notes through lecture_08. Everything that we have covered in class is covered in the notes, and more. You can work with the examples in the notes in VSCode by opening the corresponding files. Don't be afraid to "play around" with the examples in VSCode. Doing to will really solidify your understanding.
#2. Is Prod Commutative?
If you have bread and cheese can you always get yourself cheese and bread? Let's convert this question into one that's both more abstract and general as well as formal (mathematical).
If you're given types, α and β, and an arbitrary ordered pair of type α × β, can you always construct and return a value of type β × α? Prove that the answer is yes by writing a function that takes any value of type α × β value and that returns a value of type β × α. Call your function prod_comm.
defprod_comm {prod_comm: {α β : Type} → α × β → β × ααα: Typeβ :β: TypeType } :Type: Type 1α ×α: Typeβ →β: Typeβ ×β: Typeα | _ =>α: Type
Is the transformation from α × β to β × α reversible? That is, given types α and β (in that order), then if you have any term of type β × α, can you always convert it into a term of type α × β? Prove it by defining a function of the appropriate type. Call it prod_com_reverse.
-- Here:
#3: Associativity of Prod
Suppose you have bread and (cheese and jam). Can you have (bread and cheese) and jam (just grouping the ands differently)? Let's again turn this into an abstract, general, and formal question, using α, β, and γ as names instead, of bread, cheese, and jam.
Suppose α, β, and γ are arbitrary types. If you're given an arbitrary value of type α × (β × γ), can you can always produce a value of type (α × β) × γ?
To show that you can, write a function of type { α β γ : Type} → (α × (β × γ)) → ((α × β) × γ). Call it prod_assoc. We declare the type parameters before the colon in our skeleton definition so that you don't have to match on them. Hint: You can use ordered pair notation to match the input value.
-- Here def prod_assoc { α β γ : Type} : α × (β × γ) → (α × β) × γ | _ =>
Prove that the conversion works in the reverse direction as well, from (α × β) × γ to α × (β × γ) by defining a function, prod_assoc_reverse accordingly.
-- Here:
#4. Is Sum Commutative?
Suppose you have either bread or cheese. Can you always have either cheese or bread? In other words are sums commutative? That's the technical word that we use for any operator, such as +, with the property that a + b is equivalent to b + a.
Once again let's formulate the question abstractly, in a general way, and with mathematical precision.
If you have either a value of type α or a value of type β, then do you have either a value of type β or a value of type α? The answer should be obvious. To prove it, define a function, that, when applied to any term of type α ⊕ β, returns a value of type β ⊕ α. Call it sum_comm.
Note that in the outline code we provide we use a syntax that is a bit new. Re-read the material in the notes if necessary. We declare the type of sum_com then use a := followed by a lambda expression that gives the function definition. That expression, in turn uses an explcit match statement. The form you've mostly seen up to now is really just notational shorthand for this kind of definition.
def sum_comm { α β : Type} : α ⊕ β → β ⊕ α := fun s => match s with | _ => _
Can you always convert a term of type β ⊕ α into one of type α ⊕ β? Prove it by writing a function that does it. Call is sum_comm_reverse.
-- Here:
#5: Is Sum Associative?
If you have bread or (cheese or jam), can you always have (bread or cheese) or jam? In other words, are sum types associative? That's the word we use for an operator with the property that a + (b + c) is equivalent to (a + b) + c. You can associate the arguments differently without really changing the meaning.
So, if you have an arbitrary value of type α ⊕ (β ⊕ γ) can you construct a value of type (α ⊕ β) ⊕ γ? If you answer yes, prove it by defining a function of type α ⊕ (β ⊕ γ) → (α ⊕ β) ⊕ γ. Call it sum_assoc.
Hint: Consider two cases for α ⊕ (β ⊕ γ), and within the "right" case, consider two further cases. You can solve this problem with three matching patterns: one for the first case and two for the second, each of which starts with a Sum.inr. You will need to use "nested" expressions involving Sum.inl and Sum.inr in both matching and to define return result values.
def sum_assoc { α β γ : Type} : α ⊕ (β ⊕ γ) → (α ⊕ β) ⊕ γ | (Sum.inl a) => (Sum.inl) | (Sum.inr (Sum.inr: {α : Type ?u.458} → {β : Type ?u.457} → β → α ⊕ βSum.inlSum.inl: {α : Type ?u.463} → {β : Type ?u.462} → α → α ⊕ βb)) =>b: β| _ =>
Does this conversion also work in reverse? Prove it with a function that takes a term of the second sum type (in the preceding example) as an argument and that returns a value of the first sum type as a result.
-- Here:
#6. Products Distribute Over Sums
If you have bread and (cheese or jam) do you have (bread and cheese) or (bread and jam)? We think so. Before you move on, think about it!
Define prod_dist_sum : α × (β ⊕ γ) → (α × β) ⊕ (α × γ). In other words, if you have a value that includes (1) a value of type α and (2) either a value of type β or a value of type γ, then you can derive a value that is either an α value and a β value, or an α value and a γ value.
def prod_dist_sum {α β γ : Type} :| _ => _ | _ => _
Does the preceding principle work in reverse? In other words, if you have (α × β) ⊕ (α × γ) can you derive α × (β ⊕ γ)? Concretely, if you have either bread and cheese or bread and jam. do you have bread and either cheese or jam? Prove it with a function, that converts any value of type (α × β) ⊕ (α × γ) into one of type α × (β ⊕ γ).
-- Here:
In the forward (first) direction we can say that products distribute over sums, just as, say, 4 * (2 + 3) is the same as (4 * 2) + (4 * 3)*. In the reverse direction, we can say that can factor out the common factor, 4. So in a sense, we're now doing Algebra 1 but with sandwiches!
#8. Sum Elimination
Suppose you're given: (1) types called rain, sprinkler, and wet; (2) a value of type rain ⊕ sprinkler; and (3), two functions, of types rain → wet and sprinkler → wet. Show that you can construct and return a value of type wet. Do this by defining a function called its_wet, that, if given values of those types as arguments, returns a value of type wet.
-- Here
Now rewrite your function using the type names, α, γ, and β instead of rain, sprinkler and wet. Call it sum_elim.
-- Here:
You should now better understand how to program with arbitrary values of arbitrary sum types. To do so, you need to be able to derive a result of the return type, γ from either of the possible types in the sum: from a value of either type α or β.
Wrap-Up
The programs (functions) we've asked you to write for this homework are deeply important, in that they correspond directly to fundamental principles of logical reasoning. The until now hidden purpose of this assignment has been to warm you up to this profound idea.
Homework 5: Inhabitedness and Induction
The PURPOSE of this homework is to greatly strengthen your understanding of reasoning with sum and product types along with properties of being inhabited or not.
READ THIS: The collaboration rule for this homework is that you may not collaborate. You can ask friends and colleagues to help you understand the class material, but you may not discuss any of these homework problems with anyone other than one of the instructors or TAs.
Finally, what you're seeing here is the FIRST set of questions on this homework, giving you an opportunity to deepen your understanding of the Empty type and its uses.
PART 1: Inhabitedness and Logical Negation
Of particular importance in these questions is the idea that having a function value (implementation) of type α → Empty proves that α is uninhabited, in that if there were a value (a : α) then you'd be able to derive a value of type Empty, and that simply can't be done, so there must be no such (a : α). That's the great idea that we reached at the end of lecture_09.
More concretely every time you see function type that looks like (α → Empty) in what follows, you can read it as saying there is no value of type α. Second, if youwant to return a result of type (α → Empty), to showing that there can be no α value, then you need to return a function; and you will often want to do so by writing the return value as a lambda expression.
#1 Not Jam or Not Cheese Implies Not Jam and Cheese
Suppose you don't have cheese OR you don't have jam. Then it must be that you don't have (cheese AND jam). Before you go on, think about why this has to be true. Here's a proof of it in the form of a function. The function takes jam and cheese implicitly as types. It takes a value that either indicates there is no jam, or a value that indicates that there's no cheese, and you are to construct a value that shows that there can be no jam and cheese. It works by breaking the first argument into two cases: either a proof that there is no jam (there are no values of this type), or a proof that there is no cheese, and shows in either case that there can be no jam AND cheese.
New Addition: no (α : Type) := α → Empty
We can make the logical intent of our types and computations clearer by introducing a shorthand, no α for the type α → Empty. Then in each place where a type like *α → Empty appears in this homework, replace it with no α. Use the right local names in each instance, of course.
def no: Type → Type
no (α: Type
α : Type: Type 1
Type) := α: Type
α → Empty: Type
Empty
We've now replaced each α → Empty with no α. We suggest that you go ahead and use no wherever doing so makes the logical meaning clearer.
defnot_either_not_both {not_either_not_both: {jam cheese : Type} → no jam ⊕ no cheese → no (jam × cheese)jamjam: Typecheese } : ((cheese: Typenono: Type → Typejam) ⊕ (jam: Typenono: Type → Typecheese)) → (cheese: Typeno (no: Type → Typejam ×jam: Typecheese)) |cheese: TypeSum.inlSum.inl: {α : Type ?u.38} → {β : Type ?u.37} → α → α ⊕ βnojam => (funnojam: no jam_ =>_: jam × cheese) |Sum.inr _ =>Sum.inr: {α : Type ?u.65} → {β : Type ?u.64} → β → α ⊕ β
#2: Not One or Not the Other Implies Not Both
Now prove this principle in general by defining a function, demorgan1, of the following type. It's will be the same function, just with the names α and β for the types, rather than the more suggestive but specific names, jam and cheese.
{α β : Type} → (α → Empty ⊕ β → Empty) → (α × β → Empty).
defdemorgan1 {demorgan1: {α β : Type} → (α → Empty) ⊕ (β → Empty) → α × β → Emptyαα: Typeβ :β: TypeType} : ((Type: Type 1α →α: TypeEmpty) ⊕ (Empty: Typeβ →β: TypeEmpty)) → (Empty: Typeα ×α: Typeβ →β: TypeEmpty) | (Empty: TypeSum.inlSum.inl: {α : Type ?u.260} → {β : Type ?u.259} → α → α ⊕ βnoa) =>noa: α → Empty| (Sum.inrSum.inr: {α : Type ?u.292} → {β : Type ?u.291} → β → α ⊕ βnob) =>nob: β → Empty
#3: Not Either Implies Not One And Not The Other
Now suppose that you don't have either jam and cheese. Then you don't have jam and you don't have cheese. More generally, if you don't have an α OR a β, then you can conclude that you don't have an α Here's a function type that asserts this fact in a general way. Show it's true in general by implementing it. An implementation will show that given any types, α and β,
defdemorgan2 {demorgan2: {α β : Type} → (α ⊕ β → Empty) → (α → Empty) × (β → Empty)αα: Typeβ :β: TypeType} : (Type: Type 1α ⊕α: Typeβ →β: TypeEmpty) → ((Empty: Typeα →α: TypeEmpty) × (Empty: Typeβ →β: TypeEmpty)) |Empty: Typenoaorb =>noaorb: α ⊕ β → Empty
#4: Not One And Not The Other Implies Not One Or The Other
Suppose you know that there is no α AND there is no β. Then you can deduce that there can be no (α ⊕ β) object. Again we give you the function type that expresses this idea, and you must show it's true by implementing the function. Hint: You might want to use an explicit match expression in writing your solution.
defdemorgan3 {demorgan3: {α β : Type} → (α → Empty) × (β → Empty) → α ⊕ β → Emptyαα: Typeβ :β: TypeType} : ((Type: Type 1α →α: TypeEmpty) × (Empty: Typeβ →β: TypeEmpty)) → ((Empty: Typeα ⊕α: Typeβ) →β: TypeEmpty) | _ =>Empty: Type
PART 2
The following problems aim to strengthen your understanding of inductive type definitions and recusrive functions.
-- Here are some named Nat values, for testing
def n0 := Nat.zero
def n1 := Nat.succ n0
def n2 := Nat.succ n1
def n3 := Nat.succ n2
def n4 := Nat.succ n3
def n5 := Nat.succ n4
#1. Pattern Matching Enables Destructuring
#1: Defne a function, pred: Nat → Nat, that takes an any Nat, n, and, if n is zero, returns zero, otherwise analyze n as (Nat.succ n') and return n'. Yes this question should be easy. Be sure you understand destructuring and pattern matching.
-- Here -- Test cases3 -- expect 23: ?m.7760 -- expect 00: ?m.780
#2. Big Doll from Smaller One n Times
Write a function, mk_doll : Nat → Doll, that takes any natural number argument, n, and that returns a doll n shells deep. The verify using #reduce that (mk_doll 3) returns the same doll as d3.
-- Answer here -- test cases #check33: ?m.78433: ?m.786
#3. A Boolean Nat Equality Predicate
Write a function, nat_eq : Nat → Nat → Bool, that takes any two natural numbers and that returns Boolean true if they're equal, and false otherwise. Finish off the definition by filling the remaining hole (_).
defnat_eq :nat_eq: Nat → Nat → BoolNat →Nat: TypeNat →Nat: TypeBool |Bool: Type0,0: Nat0 =>0: Nattrue |true: Bool0,0: Natn' + 1 =>n': Natfalse |false: Booln' + 1,n': Nat0 =>0: Natfalse | (false: Booln' + 1), (n': Natm' + 1) =>m': Nat-- a few tests
#4. Natural Number Less Than Or Equal
Write a function, nat_le : Nat → Nat → Bool, that takes any two natural numbers and that returns Boolean true if the first value is less than or equal to the second, and false otherwise. Hint: what are the relevant cases? Match to destructure them then return the right result in each case.
-- Here
#5. Nat Number Addition
Complete this function definition to implement a natural number addition function.
def add : Nat → Nat → Nat | m, 0 => m | m, (Nat.succ n') =>-- hint: recursion -- Some test casesaddadd: Nat → Nat → Nat00: Nat0 -- expect 00: Nataddadd: Nat → Nat → Nat55: Nat0 -- expect 50: Nataddadd: Nat → Nat → Nat00: Nat5 -- expect 55: Nataddadd: Nat → Nat → Nat55: Nat4 -- expect 94: Nataddadd: Nat → Nat → Nat44: Nat5 -- expect 95: Nataddadd: Nat → Nat → Nat55: Nat5 -- expect 105: Nat
#6. Natural Number Multiplication
Complete this function definition to implement a natural number multiplication function. You can't use Lean's Nat multiplication function. Your implementation should use productively the add function you just definied. Wite a few test cases to show that it appears to be working.
defmul :mul: Nat → Nat → NatNat →Nat: TypeNat →Nat: TypeNat |Nat: Typem,m: Nat0 =>0: Nat0 |0: Natm, (m: NatNat.succNat.succ: Nat → Natn') =>n': Natadd (add: Nat → Nat → Nat) ()
Sum Binary Nat Function Over Range 0 to n
Define a function, sum_f, that takes a function, f : Nat → Nat and a natural number n, and that returns the sum of all of the values of (f k) for k ranging from 0 to n.
Compute expected results by hand for a few test cases and write the tests using #reduce. For example, you might use the squaring function as an argument, with a nat, n, to obtain the sum of the squares of all the numbers from 0 to and including n.
defsum_f : (sum_f: (Nat → Nat) → Nat → NatNat →Nat: TypeNat) →Nat: TypeNat →Nat: TypeNat |Nat: Typef,f: Nat → Nat0 =>0: Nat|f,f: Nat → Natn' + 1 =>n': Nat
Homework 6
The established rules apply. Do this work on your own.
This homework will test and strengthen your understanding of inductive data types, including Nat and List α, and the use of recursive functions to process and construct values of such types.
The second part will test and develop your knowledge and understanding of formal languages, and propositional logic, (PL) in particular, including the syntax and semantics of PL, and translations between formal statements in PL and corresponding concrete English-language examples.
Part 1: Inductive Types and Recursive Functions
#1: Iterated Function Application
Here are two functions, each of which takes a function, f, as an argument, and an argument, a, to that function, and returns the result of applying f to a one or more times. The first function just applies f to a once. The second function applies f to a twice. Be sure you fully understand these definitions before proceeding.
defapply {apply: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αff: α → αa defa: αapply_twice {apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αf (f: α → αff: α → αa)a: α
Your job now is to define a function, apply_n, that takes a function, f, a natural number, n, and an argument, a, and that returns the result of applying f to a n times. Define the result of applying f to a zero times as just a. Hint: recursion on n. That is, you will have two cases: where n is 0; and where n is greater than 0, and can thus be written as (1 + n') for some smaller natural number, n'.
-- Answer here defapply_n {apply_n: {α : Type} → (α → α) → α → Nat → αα :α: TypeType} : (Type: Type 1α →α: Typeα) →α: Typeα →α: TypeNat →Nat: Typeα |α: Typef,f: α → αa,a: α0 =>0: Nat|f,f: α → αa, (a: αn' + 1) =>n': Nat-- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times-- expect 4 -- apply "double" to 2 four times-- expect 32 -- apply "square" to 2 four times-- expect 65536
A Short Introduction to Lists
The polymorphic data type, List α, is used to represent lists of values of any type, α. The List type builder provides two constructors: one to create and empty list of α values, and one to construct a new non-empty list from a new element (head, of type α) and a one smaller list (tail, of type List α). Here's how the List type builder is defined (simplied just a tad).
namespace cs2120
inductive List (α : Type): Type
| nil : List α
| cons (h : α) (t : List α) : List α
end cs2120
Lean defines three useful notations for creating and destructuring lists.
- [] means List.nil
- h::t means cons h t
- [1, 2, 3] means the list, 1::[2,3]
- which means 1::2::[3]
- which means 1::2::3::[]
- which means cons 1 (cons 2 (cons 3 nil))
([] :[]: List NatListList: Type → TypeNat)Nat: Type1::[1: Nat2,2: Nat3]3: Nat[1,1: Nat2,2: Nat3]3: Nat
You can use these notations when pattern matching to analyze arguments. Here we show how this work by defining a function that takes a list and returns either (using a sum type) unit to represent the case where there is no first element, or the value at the head of the list.
deffirst_elt :first_elt: List Nat → Unit ⊕ NatListList: Type → TypeNat →Nat: TypeUnit ⊕Unit: TypeNat | [] =>Nat: TypeSum.inlSum.inl: {α β : Type} → α → α ⊕ βUnit.unit |Unit.unit: Unith::_ =>h: NatSum.inrSum.inr: {α β : Type} → β → α ⊕ βhh: Natfirst_eltfirst_elt: List Nat → Unit ⊕ Nat[] -- expect Sum.inl unit[]: List Natfirst_elt [first_elt: List Nat → Unit ⊕ Nat1,1: Nat2] -- expect 1 (left)2: Nat
#2: List length function
Lists are defined inductively in Lean. A list of values of some type α is either the empty list of α values, denoted [], or an α value followed by a shorter list of α values, denoted h::t, where h (the head of the list) is a single value of type α, and t is a shorter list of α values. The base case is of course the empty list. Define a function called len that takes a list of values of any type, α, and that returns the length of the list.
deflen {len: {α : Type} → List α → Natα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeNat | _ =>Nat: Type__: Nat-- expect 0-- expect 3-- expect 3
#3: Reduce a List of Bool to a Bool
Define a function that takes a list of Boolean values and that "reduces" it to a single Boolean value, which it returns, where the return value is true if all elements are true and otherwise is false. Call your function reduce_and.
Hint: the answer is the result of applying and to two arguments: (1) the first element, and (2) the result of recursively reducing the rest of the list. You will have to figure out what the return value for the base case of an empty list needs to be for your function to work in all cases.
def reduce_and : List Bool → Bool | _ => _-- Test cases-- expect true-- expect false-- expect true-- expect false
#4 Negate a List of Booleans
Define a function, call it (map_not) that takes a list of Boolean values and returns a list of Boolean values, where each entry in the returned list is the negation of the corresonding element in the given list of Booleans. For example, map_not [true, false] should return [false, true].
def map_not : List Bool → List Bool | [] => [] | h::t =>-- hint: use :: to construct answer -- test cases-- exect []-- expect [false, true]
#5 List the First n Natural Numbers
Define a function called countdown that takes a natural number argument, n, and that returns a list of all the natural numbers from n to 0, inclusive.
-- Your answer here -- test cases #eval0 -- expect [0] #eval0: ?m.341375 -- expect [5,4,3,2,1,0]5: ?m.34156
#6: List concatenation
Suppose Lean didn't provide the List.append function, denoted ++. Write your own list append function. Call it concat. For any type α, it takes two arguments of type List α and returns a result of type List α, the result of appending the second list to the first. Hint: do case analysis on the first argument, and think about this function as an analog of natural number addition.
-- Here defconcat {concat: {α : Type} → (x : List ?m.34217) → (x_1 : ?m.34201 x) → ?m.34202 x x_1α :α: TypeType} :Type: Type 1| [], m => _ | _, _ => _ -- Test cases #eval[1,2,3] [] -- expect [1,2,3] #eval[1,2,3] []: ?m.34261[] [1,2,3] -- expect [1,2,3] #eval[] [1,2,3]: ?m.34280[1,2] [3,4] -- expect [1,2,3,4][1,2] [3,4]: ?m.34299
#7: Lift Element to List
Write a function, pure', that takes a value, a, of any type α, and that returns a value of type List α containing just that one element.
-- Here #eval"Hi" -- expect ["Hi"]"Hi": ?m.34318
Challenge: List Reverse
Define a function, list_rev, that takes a list of values of any type and that returns it in reverse order. Hint: you can't use :: with a single value on the right; it needs a list on the right. Instead, consider using concat.
-- Answer here:
Part 2: Propositional Logic: Syntax and Semantics
Forthcoming as an update to this file.
Homework 6
The established rules apply. Do this work on your own.
This homework will test and strengthen your understanding of inductive data types, including Nat and List α, and the use of recursive functions to process and construct values of such types.
The second part will test and develop your knowledge and understanding of formal languages, and propositional logic, (PL) in particular, including the syntax and semantics of PL, and translations between formal statements in PL and corresponding concrete English-language examples.
Part 1: Inductive Types and Recursive Functions
#1: Iterated Function Application
Here are two functions, each of which takes a function, f, as an argument, and an argument, a, to that function, and returns the result of applying f to a one or more times. The first function just applies f to a once. The second function applies f to a twice. Be sure you fully understand these definitions before proceeding.
defapply {apply: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αff: α → αa defa: αapply_twice {apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αf (f: α → αff: α → αa)a: α
Your job now is to define a function, apply_n, that takes a function, f, a natural number, n, and an argument, a, and that returns the result of applying f to a n times. Define the result of applying f to a zero times as just a. Hint: recursion on n. That is, you will have two cases: where n is 0; and where n is greater than 0, and can thus be written as (1 + n') for some smaller natural number, n'.
-- Answer here defapply_n {apply_n: {α : Type} → (α → α) → α → Nat → αα :α: TypeType} : (Type: Type 1α →α: Typeα) →α: Typeα →α: TypeNat →Nat: Typeα |α: Type,a,a: α0 =>0: Nata |a: αf,f: α → αa, (a: αn' + 1) =>n': Natf (f: α → αapply_napply_n: {α : Type} → (α → α) → α → Nat → αff: α → αaa: αn') -- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four timesn': Natapply_napply_n: {α : Type} → (α → α) → α → Nat → αNat.succNat.succ: Nat → Nat00: Nat4 -- expect 4 -- apply "double" to 2 four times4: Natapply_n (λapply_n: {α : Type} → (α → α) → α → Nat → αn =>n: Nat2*2: Natn)n: Nat22: Nat4 -- expect 32 -- apply "square" to 2 four times4: Natapply_n (λapply_n: {α : Type} → (α → α) → α → Nat → αn =>n: Natn^n: Nat2)2: Nat22: Nat4 -- expect 655364: Nat
A Short Introduction to Lists
The polymorphic data type, List α, is used to represent lists of values of any type, α. The List type builder provides two constructors: one to create and empty list of α values, and one to construct a new non-empty list from a new element (head, of type α) and a one smaller list (tail, of type List α). Here's how the List type builder is defined (simplied just a tad).
namespace cs2120
inductive List: Type → Type
List (α: Type
α : Type: Type 1
Type): Type: Type 1
Type
| nil: {α : Type} → List α
nil : List: Type → Type
List α: Type
α
| cons: {α : Type} → α → List α → List α
cons (h: α
h : α: Type
α) (t: List α
t : List: Type → Type
List α: Type
α) : List: Type → Type
List α: Type
α
end cs2120
Lean defines three useful notations for creating and destructuring lists.
- [] means List.nil
- h::t means cons h t
- [1, 2, 3] means the list, 1::[2,3]
- which means 1::2::[3]
- which means 1::2::3::[]
- which means cons 1 (cons 2 (cons 3 nil))
([] :[]: List NatListList: Type → TypeNat)Nat: Type1::[1: Nat2,2: Nat3]3: Nat[1,1: Nat2,2: Nat3]3: Nat
You can use these notations when pattern matching to analyze arguments. Here we show how this work by defining a function that takes a list and returns either (using a sum type) unit to represent the case where there is no first element, or the value at the head of the list.
deffirst_elt :first_elt: List Nat → Unit ⊕ NatListList: Type → TypeNat →Nat: TypeUnit ⊕Unit: TypeNat | [] =>Nat: TypeSum.inlSum.inl: {α β : Type} → α → α ⊕ βUnit.unit |Unit.unit: Unith::_ =>h: NatSum.inrSum.inr: {α β : Type} → β → α ⊕ βhh: Natfirst_eltfirst_elt: List Nat → Unit ⊕ Nat[] -- expect Sum.inl unit[]: List Natfirst_elt [first_elt: List Nat → Unit ⊕ Nat1,1: Nat2] -- expect 1 (left)2: Nat
#2: List length function
Lists are defined inductively in Lean. A list of values of some type α is either the empty list of α values, denoted [], or an α value followed by a shorter list of α values, denoted h::t, where h (the head of the list) is a single value of type α, and t is a shorter list of α values. The base case is of course the empty list. Define a function called len that takes a list of values of any type, α, and that returns the length of the list.
deflen {len: {α : Type} → List α → Natα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeNat | [] =>Nat: Type0 |0: Nat::t =>t: List α1 +1: Natlenlen: {α : Type} → List α → Nattt: List α@lenlen: {α : Type} → List α → NatNatNat: Type[] -- expect 0[]: List Natlen [len: {α : Type} → List α → Nat0,0: Nat1,1: Nat2] -- expect 32: Natlen [len: {α : Type} → List α → Nat"I","I": String"Love","Love": String"Logic!"] -- expect 3"Logic!": String
#3: Reduce a List of Bool to a Bool
Define a function that takes a list of Boolean values and that "reduces" it to a single Boolean value, which it returns, where the return value is true if all elements are true and otherwise is false. Call your function reduce_and.
Hint: the answer is the result of applying and to two arguments: (1) the first element, and (2) the result of recursively reducing the rest of the list. You will have to figure out what the return value for the base case of an empty list needs to be for your function to work in all cases.
defreduce_and :reduce_and: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typetrue |true: Boolh::h: Boolt =>t: List Boolandand: Bool → Bool → Boolh (h: Boolreduce_andreduce_and: List Bool → Boolt) -- Test casest: List Boolreduce_and [reduce_and: List Bool → Booltrue] -- expect truetrue: Boolreduce_and [reduce_and: List Bool → Boolfalse] -- expect falsefalse: Boolreduce_and [reduce_and: List Bool → Booltrue,true: Booltrue] -- expect truetrue: Boolreduce_and [reduce_and: List Bool → Boolfalse,false: Booltrue] -- expect falsetrue: Bool
#4 Negate a List of Booleans
Define a function, call it (map_not) that takes a list of Boolean values and returns a list of Boolean values, where each entry in the returned list is the negation of the corresonding element in the given list of Booleans. For example, map_not [true, false] should return [false, true].
defmap_not :map_not: List Bool → List BoolListList: Type → TypeBool →Bool: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Boolt =>t: List Boolnotnot: Bool → Boolh ::h: Boolmap_notmap_not: List Bool → List Boolt -- hint: use :: to construct answer -- test casest: List Boolmap_notmap_not: List Bool → List Bool[] -- exect [][]: List Boolmap_not [map_not: List Bool → List Booltrue,true: Boolfalse] -- expect [false, true]false: Bool
#5 List the First n Natural Numbers
Define a function called countdown that takes a natural number argument, n, and that returns a list of all the natural numbers from n to 0, inclusive.
-- Your answer here defcountdown :countdown: Nat → List NatNat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Natn' + 1 => (n': Natn' +n': Nat1)::1: Natcountdowncountdown: Nat → List Natn' -- test casesn': Natcountdowncountdown: Nat → List Nat0 -- expect [0]0: Natcountdowncountdown: Nat → List Nat5 -- expect [5,4,3,2,1,0]5: Nat
#6: List concatenation
Suppose Lean didn't provide the List.append function, denoted ++. Write your own list append function. Call it concat. For any type α, it takes two arguments of type List α and returns a result of type List α, the result of appending the second list to the first. Hint: do case analysis on the first argument.
-- Here defconcat {concat: {α : Type} → List α → List α → List αα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeListList: Type → Typeα →α: TypeListList: Type → Typeα | [],α: Typem =>m: List αm |m: List αh::h: αt,t: List αm =>m: List αh::h: αconcatconcat: {α : Type} → List α → List α → List αtt: List αm -- Test casesm: List αconcat [concat: {α : Type} → List α → List α → List α1,1: Nat2,2: Nat3]3: Nat[] -- expect [1,2,3][]: List Natconcatconcat: {α : Type} → List α → List α → List α[] [[]: List Nat1,1: Nat2,2: Nat3] -- expect [1,2,3]3: Natconcat [concat: {α : Type} → List α → List α → List α1,1: Nat2] [2: Nat3,3: Nat4] -- expect [1,2,3,4]4: Nat
#7: Lift Element to List
Write a function, pure', that takes a value, a, of any type α, and that returns a value of type List α containing just that one element.
-- Here defpure' :pure': String → List StringString →String: TypeListList: Type → TypeString |String: Types => [s: Strings]s: Stringpure'pure': String → List String"Hi" -- expect ["Hi"]"Hi": String
Challenge: List Reverse
Define a function, list_rev, that takes a list of values of any type and that returns it in reverse order. Hint: you can't use :: with a single value on the right; it needs a list on the right. Instead, consider using concat.
-- Answer here:
def rev: {α : Type} → List α → List α
rev {α: Type
α : Type: Type 1
Type}: List: Type → Type
List α: Type
α → List: Type → Type
List α: Type
α
| [] => []: List α
[]
| h: α
h::t: List α
t => t: List α
t++[h: α
h]
End of Exam Practice Part 1
A few words on destructured argument declarations
Sometimes one needs to express a function as a lambda expression, with an an ordered pair (p : α x β) as an argument. To use p, one will often have to destructure it: to analyze it as (a, b). With names for the two parts of (the term representing) the ordered pair, you can define such functions as fst (just return a), snd (just return b), and swap (return (b, a)).
Argument Not Analyzed
This function takes a pair; doesn't analyze it; and in this simple example, just returns, it. Polymorphic functions, capable of handling objects of any type, often handle object without ever inspecting them.
fun {αα: Typeβ :β: TypeType} (Type: Type 1p :p: α × βα ×α: Typeβ) =>β: Typepp: α × β
Pair Analyzed As (a, b) By match Expression
If a function has to take as an argument a single product object, p, that's fine; you just destructure p explicitly using a match expression. Hare are examples using a lamdba expression for the ordered pair swap function, taking each (a,b) pair to (b,a).
λ {αα: Typeβ :β: TypeType} (Type: Type 1p :p: α × βα ×α: Typeβ) -- pair object => matchβ: Typep with -- analyze p ... | (p: α × βa,a: αb) => -- ... as (a, b) (b: βb,b: βa) -- return (b,a) -- Application of function. Expect ("Dolly", " Hello ")a: α(λ {αα: Typeβ :β: TypeType} -- function (Type: Type 1p :p: α × βα ×α: Typeβ) => matchβ: Typep with | (p: α × βa,a: αb) => (b: βb,b: βa) ) -- end function (a: α" Hello "," Hello ": String"Dolly") -- argument"Dolly": String
Destructured Arguments
A great trick is to express a pair argument in already destructured form, (a,b). Here's the swap function written using this syntactic feature. Note that we have replaced p with (a, b). That's the trick.
fun {:Type} -- implicit type arguments ((Type: Type 1a,a: αb) : α × β ) -- *destructured pair, (a, b)* => (b: βb,b: βa) -- swap function is now trivial -- And here's an example applicationa: α(λ {:Type} -- Expect ("Dolly", " Hello ") (Type: Type 1a,a: αb) => (b: βb,b: βa)) (a: α" Hello "," Hello ": String"Dolly")"Dolly": String
The TL;DR Takeaway
You can write function (lambda) expressions with ordered pairs objects as arguments, but *expressed in the their destructured form, (a, b). You can then write return values as functions of a and b.
Homework 6
The established rules apply. Do this work on your own.
This homework will test and strengthen your understanding of inductive data types, including Nat and List α, and the use of recursive functions to process and construct values of such types.
The second part will test and develop your knowledge and understanding of formal languages, and propositional logic, (PL) in particular, including the syntax and semantics of PL, and translations between formal statements in PL and corresponding concrete English-language examples.
Part 1: Inductive Types and Recursive Functions
#1: Iterated Function Application
Here are two functions, each of which takes a function, f, as an argument, and an argument, a, to that function, and returns the result of applying f to a one or more times. The first function just applies f to a once. The second function applies f to a twice. Be sure you fully understand these definitions before proceeding.
defapply {apply: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αff: α → αa defa: αapply_twice {apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αf (f: α → αff: α → αa)a: α
Your job now is to define a function, apply_n, that takes a function, f, a natural number, n, and an argument, a, and that returns the result of applying f to a n times. Define the result of applying f to a zero times as just a. Hint: recursion on n. That is, you will have two cases: where n is 0; and where n is greater than 0, and can thus be written as (1 + n') for some smaller natural number, n'.
-- Answer here defapply_n {apply_n: {α : Type} → (α → α) → α → Nat → αα :α: TypeType} : (Type: Type 1α →α: Typeα) →α: Typeα →α: TypeNat →Nat: Typeα |α: Typef,f: α → αa,a: α0 =>0: Nat|f,f: α → αa, (a: αn' + 1) =>n': Nat-- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times-- expect 4 -- apply "double" to 2 four times-- expect 32 -- apply "square" to 2 four times-- expect 65536
A Short Introduction to Lists
The polymorphic data type, List α, is used to represent lists of values of any type, α. The List type builder provides two constructors: one to create and empty list of α values, and one to construct a new non-empty list from a new element (head, of type α) and a one smaller list (tail, of type List α). Here's how the List type builder is defined (simplied just a tad).
namespace cs2120
inductive List (α : Type): Type
| nil : List α
| cons (h : α) (t : List α) : List α
end cs2120
Lean defines three useful notations for creating and destructuring lists.
- [] means List.nil
- h::t means cons h t
- [1, 2, 3] means the list, 1::[2,3]
- which means 1::2::[3]
- which means 1::2::3::[]
- which means cons 1 (cons 2 (cons 3 nil))
([] :[]: List NatListList: Type → TypeNat)Nat: Type1::[1: Nat2,2: Nat3]3: Nat[1,1: Nat2,2: Nat3]3: Nat
You can use these notations when pattern matching to analyze arguments. Here we show how this work by defining a function that takes a list and returns either (using a sum type) unit to represent the case where there is no first element, or the value at the head of the list.
deffirst_elt :first_elt: List Nat → Unit ⊕ NatListList: Type → TypeNat →Nat: TypeUnit ⊕Unit: TypeNat | [] =>Nat: TypeSum.inlSum.inl: {α β : Type} → α → α ⊕ βUnit.unit |Unit.unit: Unith::_ =>h: NatSum.inrSum.inr: {α β : Type} → β → α ⊕ βhh: Natfirst_eltfirst_elt: List Nat → Unit ⊕ Nat[] -- expect Sum.inl unit[]: List Natfirst_elt [first_elt: List Nat → Unit ⊕ Nat1,1: Nat2] -- expect 1 (left)2: Nat
#2: List length function
Lists are defined inductively in Lean. A list of values of some type α is either the empty list of α values, denoted [], or an α value followed by a shorter list of α values, denoted h::t, where h (the head of the list) is a single value of type α, and t is a shorter list of α values. The base case is of course the empty list. Define a function called len that takes a list of values of any type, α, and that returns the length of the list.
deflen {len: {α : Type} → List α → Natα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeNat | _ =>Nat: Type__: Nat-- expect 0-- expect 3-- expect 3
#3: Reduce a List of Bool to a Bool
Define a function that takes a list of Boolean values and that "reduces" it to a single Boolean value, which it returns, where the return value is true if all elements are true and otherwise is false. Call your function reduce_and.
Hint: the answer is the result of applying and to two arguments: (1) the first element, and (2) the result of recursively reducing the rest of the list. You will have to figure out what the return value for the base case of an empty list needs to be for your function to work in all cases.
def reduce_and : List Bool → Bool | _ => _-- Test cases-- expect true-- expect false-- expect true-- expect false
#4 Negate a List of Booleans
Define a function, call it (map_not) that takes a list of Boolean values and returns a list of Boolean values, where each entry in the returned list is the negation of the corresonding element in the given list of Booleans. For example, map_not [true, false] should return [false, true].
def map_not : List Bool → List Bool | [] => [] | h::t =>-- hint: use :: to construct answer -- test cases-- exect []-- expect [false, true]
#5 List the First n Natural Numbers
Define a function called countdown that takes a natural number argument, n, and that returns a list of all the natural numbers from n to 0, inclusive.
-- Your answer here -- test cases #eval0 -- expect [0] #eval0: ?m.341375 -- expect [5,4,3,2,1,0]5: ?m.34156
#6: List concatenation
Suppose Lean didn't provide the List.append function, denoted ++. Write your own list append function. Call it concat. For any type α, it takes two arguments of type List α and returns a result of type List α, the result of appending the second list to the first. Hint: do case analysis on the first argument, and think about this function as an analog of natural number addition.
-- Here defconcat {concat: {α : Type} → (x : List ?m.34217) → (x_1 : ?m.34201 x) → ?m.34202 x x_1α :α: TypeType} :Type: Type 1| [], m => _ | _, _ => _ -- Test cases #eval[1,2,3] [] -- expect [1,2,3] #eval[1,2,3] []: ?m.34261[] [1,2,3] -- expect [1,2,3] #eval[] [1,2,3]: ?m.34280[1,2] [3,4] -- expect [1,2,3,4][1,2] [3,4]: ?m.34299
#7: Lift Element to List
Write a function, pure', that takes a value, a, of any type α, and that returns a value of type List α containing just that one element.
-- Here #eval"Hi" -- expect ["Hi"]"Hi": ?m.34318
Challenge: List Reverse
Define a function, list_rev, that takes a list of values of any type and that returns it in reverse order. Hint: you can't use :: with a single value on the right; it needs a list on the right. Instead, consider using concat.
-- Answer here:
Part 2: Propositional Logic: Syntax and Semantics
Forthcoming as an update to this file.
Homework 6
The established rules apply. Do this work on your own.
This homework will test and strengthen your understanding of inductive data types, including Nat and List α, and the use of recursive functions to process and construct values of such types.
The second part will test and develop your knowledge and understanding of formal languages, and propositional logic, (PL) in particular, including the syntax and semantics of PL, and translations between formal statements in PL and corresponding concrete English-language examples.
Part 1: Inductive Types and Recursive Functions
#1: Iterated Function Application
Here are two functions, each of which takes a function, f, as an argument, and an argument, a, to that function, and returns the result of applying f to a one or more times. The first function just applies f to a once. The second function applies f to a twice. Be sure you fully understand these definitions before proceeding.
defapply {apply: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αff: α → αa defa: αapply_twice {apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α:α} : (α: Sort u_1α →α: Sort u_1α) →α: Sort u_1α →α: Sort u_1α |α: Sort u_1f,f: α → αa =>a: αf (f: α → αff: α → αa)a: α
Your job now is to define a function, apply_n, that takes a function, f, a natural number, n, and an argument, a, and that returns the result of applying f to a n times. Define the result of applying f to a zero times as just a. Hint: recursion on n. That is, you will have two cases: where n is 0; and where n is greater than 0, and can thus be written as (1 + n') for some smaller natural number, n'.
-- Answer here defapply_n {apply_n: {α : Type} → (α → α) → α → Nat → αα :α: TypeType} : (Type: Type 1α →α: Typeα) →α: Typeα →α: TypeNat →Nat: Typeα |α: Type,a,a: α0 =>0: Nata |a: αf,f: α → αa, (a: αn' + 1) =>n': Natf (f: α → αapply_napply_n: {α : Type} → (α → α) → α → Nat → αff: α → αaa: αn') -- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four timesn': Natapply_napply_n: {α : Type} → (α → α) → α → Nat → αNat.succNat.succ: Nat → Nat00: Nat4 -- expect 4 -- apply "double" to 2 four times4: Natapply_n (λapply_n: {α : Type} → (α → α) → α → Nat → αn =>n: Nat2*2: Natn)n: Nat22: Nat4 -- expect 32 -- apply "square" to 2 four times4: Natapply_n (λapply_n: {α : Type} → (α → α) → α → Nat → αn =>n: Natn^n: Nat2)2: Nat22: Nat4 -- expect 655364: Nat
A Short Introduction to Lists
The polymorphic data type, List α, is used to represent lists of values of any type, α. The List type builder provides two constructors: one to create and empty list of α values, and one to construct a new non-empty list from a new element (head, of type α) and a one smaller list (tail, of type List α). Here's how the List type builder is defined (simplied just a tad).
namespace cs2120
inductive List: Type → Type
List (α: Type
α : Type: Type 1
Type): Type: Type 1
Type
| nil: {α : Type} → List α
nil : List: Type → Type
List α: Type
α
| cons: {α : Type} → α → List α → List α
cons (h: α
h : α: Type
α) (t: List α
t : List: Type → Type
List α: Type
α) : List: Type → Type
List α: Type
α
end cs2120
Lean defines three useful notations for creating and destructuring lists.
- [] means List.nil
- h::t means cons h t
- [1, 2, 3] means the list, 1::[2,3]
- which means 1::2::[3]
- which means 1::2::3::[]
- which means cons 1 (cons 2 (cons 3 nil))
([] :[]: List NatListList: Type → TypeNat)Nat: Type1::[1: Nat2,2: Nat3]3: Nat[1,1: Nat2,2: Nat3]3: Nat
You can use these notations when pattern matching to analyze arguments. Here we show how this work by defining a function that takes a list and returns either (using a sum type) unit to represent the case where there is no first element, or the value at the head of the list.
deffirst_elt :first_elt: List Nat → Unit ⊕ NatListList: Type → TypeNat →Nat: TypeUnit ⊕Unit: TypeNat | [] =>Nat: TypeSum.inlSum.inl: {α β : Type} → α → α ⊕ βUnit.unit |Unit.unit: Unith::_ =>h: NatSum.inrSum.inr: {α β : Type} → β → α ⊕ βhh: Natfirst_eltfirst_elt: List Nat → Unit ⊕ Nat[] -- expect Sum.inl unit[]: List Natfirst_elt [first_elt: List Nat → Unit ⊕ Nat1,1: Nat2] -- expect 1 (left)2: Nat
#2: List length function
Lists are defined inductively in Lean. A list of values of some type α is either the empty list of α values, denoted [], or an α value followed by a shorter list of α values, denoted h::t, where h (the head of the list) is a single value of type α, and t is a shorter list of α values. The base case is of course the empty list. Define a function called len that takes a list of values of any type, α, and that returns the length of the list.
deflen {len: {α : Type} → List α → Natα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeNat | [] =>Nat: Type0 |0: Nat::t =>t: List α1 +1: Natlenlen: {α : Type} → List α → Nattt: List α@lenlen: {α : Type} → List α → NatNatNat: Type[] -- expect 0[]: List Natlen [len: {α : Type} → List α → Nat0,0: Nat1,1: Nat2] -- expect 32: Natlen [len: {α : Type} → List α → Nat"I","I": String"Love","Love": String"Logic!"] -- expect 3"Logic!": String
#3: Reduce a List of Bool to a Bool
Define a function that takes a list of Boolean values and that "reduces" it to a single Boolean value, which it returns, where the return value is true if all elements are true and otherwise is false. Call your function reduce_and.
Hint: the answer is the result of applying and to two arguments: (1) the first element, and (2) the result of recursively reducing the rest of the list. You will have to figure out what the return value for the base case of an empty list needs to be for your function to work in all cases.
defreduce_and :reduce_and: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typetrue |true: Boolh::h: Boolt =>t: List Boolandand: Bool → Bool → Boolh (h: Boolreduce_andreduce_and: List Bool → Boolt) -- Test casest: List Boolreduce_and [reduce_and: List Bool → Booltrue] -- expect truetrue: Boolreduce_and [reduce_and: List Bool → Boolfalse] -- expect falsefalse: Boolreduce_and [reduce_and: List Bool → Booltrue,true: Booltrue] -- expect truetrue: Boolreduce_and [reduce_and: List Bool → Boolfalse,false: Booltrue] -- expect falsetrue: Bool
#4 Negate a List of Booleans
Define a function, call it (map_not) that takes a list of Boolean values and returns a list of Boolean values, where each entry in the returned list is the negation of the corresonding element in the given list of Booleans. For example, map_not [true, false] should return [false, true].
defmap_not :map_not: List Bool → List BoolListList: Type → TypeBool →Bool: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Boolt =>t: List Boolnotnot: Bool → Boolh ::h: Boolmap_notmap_not: List Bool → List Boolt -- hint: use :: to construct answer -- test casest: List Boolmap_notmap_not: List Bool → List Bool[] -- exect [][]: List Boolmap_not [map_not: List Bool → List Booltrue,true: Boolfalse] -- expect [false, true]false: Bool
#5 List the First n Natural Numbers
Define a function called countdown that takes a natural number argument, n, and that returns a list of all the natural numbers from n to 0, inclusive.
-- Your answer here defcountdown :countdown: Nat → List NatNat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Natn' + 1 => (n': Natn' +n': Nat1)::1: Natcountdowncountdown: Nat → List Natn' -- test casesn': Natcountdowncountdown: Nat → List Nat0 -- expect [0]0: Natcountdowncountdown: Nat → List Nat5 -- expect [5,4,3,2,1,0]5: Nat
#6: List concatenation
Suppose Lean didn't provide the List.append function, denoted ++. Write your own list append function. Call it concat. For any type α, it takes two arguments of type List α and returns a result of type List α, the result of appending the second list to the first. Hint: do case analysis on the first argument.
-- Here defconcat {concat: {α : Type} → List α → List α → List αα :α: TypeType} :Type: Type 1ListList: Type → Typeα →α: TypeListList: Type → Typeα →α: TypeListList: Type → Typeα | [],α: Typem =>m: List αm |m: List αh::h: αt,t: List αm =>m: List αh::h: αconcatconcat: {α : Type} → List α → List α → List αtt: List αm -- Test casesm: List αconcat [concat: {α : Type} → List α → List α → List α1,1: Nat2,2: Nat3]3: Nat[] -- expect [1,2,3][]: List Natconcatconcat: {α : Type} → List α → List α → List α[] [[]: List Nat1,1: Nat2,2: Nat3] -- expect [1,2,3]3: Natconcat [concat: {α : Type} → List α → List α → List α1,1: Nat2] [2: Nat3,3: Nat4] -- expect [1,2,3,4]4: Nat
#7: Lift Element to List
Write a function, pure', that takes a value, a, of any type α, and that returns a value of type List α containing just that one element.
-- Here defpure' :pure': String → List StringString →String: TypeListList: Type → TypeString |String: Types => [s: Strings]s: Stringpure'pure': String → List String"Hi" -- expect ["Hi"]"Hi": String
Challenge: List Reverse
Define a function, list_rev, that takes a list of values of any type and that returns it in reverse order. Hint: you can't use :: with a single value on the right; it needs a list on the right. Instead, consider using concat.
-- Answer here:
def rev: {α : Type} → List α → List α
rev {α: Type
α : Type: Type 1
Type}: List: Type → Type
List α: Type
α → List: Type → Type
List α: Type
α
| [] => []: List α
[]
| h: α
h::t: List α
t => t: List α
t++[h: α
h]
End of Exam Practice Part 1
Homework #7 Part 2 -- Exam Practice
#1 Easy Functions [15 points]
Define a function, pythag, that takes three natural numbers, call them a, b, and c, and that returns true if a^2 + b^2 = c^2 and that returns false otherwise.
-- Define your function here -- The following test cases should then pass #eval3 4 5 -- expect true #eval3 4 5: ?m.26 7 8 -- expect false6 7 8: ?m.21
#2 Recursive Functions
Define a function, sum_cubes, that takes any natural number, n, as an argument, and that retrns the sum of the cubes of the natural numbers from 1 up to n inclusive.
-- Define your function here -- test case: sum_cubes 4 = 1 + 8 + 27 + 64 = 100 #eval4 -- expect 1004: ?m.40
#3 Product and Sum Types
Define two functions, called prod_ors_to_or_prods, and or_prods_to_prod_ors that shows that a product of sums be converted into a sum of products in a way that the result can then be converted back into the original product of sums.
As a concrete example, you might want to show that if you have an apple or an orange and you have a cup or a bowl, then you have an apple and a cup or an apple and a bowl or an orange and a cup or an orange and a bowl.
Hints: 1. Be sure you understand the reasoning before you try to define your functions. 2. Use four cases. 3. Use type-guided, top-down programming, assisted by the Lean prover to work out a solution for each case.
defprod_ors_to_or_prods {prod_ors_to_or_prods: {α β γ δ : Type} → (α ⊕ β) × (γ ⊕ δ) → α × γ ⊕ α × δ ⊕ β × γ ⊕ β × δαα: Typeββ: Typeγγ: Typeδ:δ: TypeType} : (Type: Type 1α ⊕α: Typeβ) × (β: Typeγ ⊕γ: Typeδ) →δ: Typeα ×α: Typeγ ⊕γ: Typeα ×α: Typeδ ⊕δ: Typeβ ×β: Typeγ ⊕γ: Typeβ ×β: Typeδ | _ =>δ: Type__: α × γ ⊕ α × δ ⊕ β × γ ⊕ β × δ-- Write the second function here from scratch
#4 Propositional Logic Syntax and Semantics
Extend your Homework #7 solution to implement the propositional logic iff/equivalence (↔) operator. Note that Lean does not natively define the iff Boolean operator.
Using our syntax for propositional logic, and the variable names A, O, C, and B, respectively for the propositions I have an apple, I have an orange, I have a cup, and I have a bowl write a proposition that having an orange or an apple and a bowl or a cup is equivalent to having an apple and a bowl or an apple and a cup or an orange and a bowl or an orange and a cup.
Note: There's no need here to use our implementation of propositional logic. Just write the expression here using the notation we've defined.
#5 Propositional Logic Validity
At the end of your updated Homework #7 file, use our validity checking function to check your expression for validity, in the expectation that the checker will determine that the expression is in fact valid.
UVa CS2120-002 F23 Midterm Exam
Propositional Logic: Syntax, Sematics, Satisfiability
This section of the exam simply includes our formal definition of the syntax and semantics of propositional logic and of functions that determine whether a given expression is valid, satisfiable, or unsatisfiable.
Syntax
-- variables
structure var : Type := (n: Nat)
-- connectives/operators
inductive unary_op : Type | not
inductive binary_op : Type
| and
| or
| imp
| iff
-- expressions (abstract syntax)
inductive Expr : Type
| var_exp (v : var)
| un_exp (op : unary_op) (e : Expr)
| bin_exp (op : binary_op) (e1 e2 : Expr)
-- concrete syntax
notation "{"v"}" => Expr.var_exp v
prefix:max "¬" => Expr.un_exp unary_op.not
infixr:35 " ∧ " => Expr.bin_exp binary_op.and
infixr:30 " ∨ " => Expr.bin_exp binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp binary_op.iff
Semantics
-- meanings of unary operators
def eval_un_op : unary_op → (Bool → Bool)
| unary_op.not => not
-- missing binary Boolean operators
def implies : Bool → Bool → Bool
| true, false => false
| _, _ => true
def iff : Bool → Bool → Bool
| true, true => true
| false, false => true
| _, _ => false
-- meanings of binary operators
def eval_bin_op : binary_op → (Bool → Bool → Bool)
| binary_op.and => and
| binary_op.or => or
| binary_op.imp => implies
| binary_op.iff => iff
-- The interpretation type
def Interp := var → Bool
-- The meanings of expressions "under" given interpretations
def eval_expr : Expr → Interp → Bool
| (Expr.var_exp v), i => i v
| (Expr.un_exp op e), i => (eval_un_op op) (eval_expr e i)
| (Expr.bin_exp op e1 e2), i => (eval_bin_op op) (eval_expr e1 i) (eval_expr e2 i)
Satisfiability
We built a satisfiability checker for propositional logic, in several pieces. This subsection includes all definitions.
Truth Table Input Rows
-- Nat to Binary -- You don't need to worry about the "have" part def right_bit (n : Nat) := n%2 def shift_right (n : Nat) := n/2 def:Nat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Nat1 => [1: Nat1] |1: Natn' + 2 => have : (n': Natshift_right (shift_right: Nat → Natn' +n': Nat2)) < (2: Natn' +n': Nat2) :=2: Natsorrysorry: shift_right (n' + 2) < n' + 2nat_to_bin (nat_to_bin: Nat → List Natshift_right (shift_right: Nat → Natn' +n': Nat2)) ++ [2: Natright_bit (right_bit: Nat → Natn' +n': Nat2)] -- Left pad with zeros def2: Natzero_pad :zero_pad: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Typev,v: Natl =>l: List Natzero_pad_recursive (zero_pad_recursive: Nat → List Nat → List Natv - (v: Natl.l: List Natlength))length: {α : Type} → List α → Natl wherel: List Natzero_pad_recursive :zero_pad_recursive: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Type0,0: Natl =>l: List Natl |l: List Natv'+1,v': Natl =>l: List Natzero_pad_recursivezero_pad_recursive: Nat → List Nat → List Natv' (v': Nat0::0: Natl) -- Make row of bits at index "row" padded out to "cols" wide defl: List Natmk_bit_row : (mk_bit_row: Nat → Nat → List Natrow:row: NatNat) → (Nat: Typecols :cols: NatNat) →Nat: TypeListList: Type → TypeNat |Nat: Typer,r: Natc =>c: Natzero_padzero_pad: Nat → List Nat → List Natc (c: Natnat_to_binnat_to_bin: Nat → List Natr) -- Convert list of bits to list of bools defr: Natbit_to_bool :bit_to_bool: Nat → BoolNat →Nat: TypeBool |Bool: Type0 =>0: Natfalse | _ =>false: Booltrue deftrue: Boolbit_list_to_bool_list :bit_list_to_bool_list: List Nat → List BoolListList: Type → TypeNat →Nat: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Natt => (t: List Natbit_to_boolbit_to_bool: Nat → Boolh) :: (h: Natbit_list_to_bool_listbit_list_to_bool_list: List Nat → List Boolt) -- Make row'th row of truth table with vars variables deft: List Natmk_row_bools : (mk_row_bools: Nat → Nat → List Boolrow :row: NatNat) → (Nat: Typevars :vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: Typer,r: Natv =>v: Natbit_list_to_bool_list (bit_list_to_bool_list: List Nat → List Boolmk_bit_rowmk_bit_row: Nat → Nat → List Natrr: Natv)v: Nat
Interpretations
-- Convert list of bools to interpretation
def override: Interp → var → Bool → Interp
override : Interp: Type
Interp → var: Type
var → Bool: Type
Bool → Interp: Type
Interp
| old_interp: Interp
old_interp, var: _root_.var
var, new_val: Bool
new_val =>
(λ v: _root_.var
v => if (v: _root_.var
v.n: _root_.var → Nat
n == var: _root_.var
var.n: _root_.var → Nat
n) -- when applied to var
then new_val: Bool
new_val -- return new value
else old_interp: Interp
old_interp v: _root_.var
v) -- else retur old value
def bools_to_interp: List Bool → Interp
bools_to_interp : List: Type → Type
List Bool: Type
Bool → Interp: Type
Interp
| l: List Bool
l => bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper l: List Bool
l.length: {α : Type} → List α → Nat
length l: List Bool
l
where bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper : (vars: Nat
vars : Nat: Type
Nat) → (vals: List Bool
vals : List: Type → Type
List Bool: Type
Bool) → Interp: Type
Interp
| _, [] => (λ _: var
_ => false: Bool
false)
| vars: Nat
vars, h: Bool
h::t: List Bool
t =>
let len: Nat
len := (h: Bool
h::t: List Bool
t).length: {α : Type} → List α → Nat
length
override: Interp → var → Bool → Interp
override (bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper vars: Nat
vars t: List Bool
t) (var.mk: Nat → var
var.mk (vars: Nat
vars - len: Nat
len)) h: Bool
h
-- Make an interpretation for given row with "vars" variables
def mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row : (vars: Nat
vars: Nat: Type
Nat) → (row: Nat
row: Nat: Type
Nat) → Interp: Type
Interp
| v: Nat
v, r: Nat
r => bools_to_interp: List Bool → Interp
bools_to_interp (mk_row_bools: Nat → Nat → List Bool
mk_row_bools r: Nat
r v: Nat
v)
-- Given number of variables, return list of interpretations
def mk_interps: Nat → List Interp
mk_interps (vars: Nat
vars : Nat: Type
Nat) : List: Type → Type
List Interp: Type
Interp :=
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper (2: Nat
2^vars: Nat
vars) vars: Nat
vars
where mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper : (rows: Nat
rows : Nat: Type
Nat) → (vars: Nat
vars : Nat: Type
Nat) → List: Type → Type
List Interp: Type
Interp
| 0: Nat
0, _ => []: List Interp
[]
| (n': Nat
n' + 1), v: Nat
v => (mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row v: Nat
v n': Nat
n')::mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper n': Nat
n' v: Nat
v
-- Count the number of variables in a given expression
def max_variable_index: Expr → Nat
max_variable_index : Expr: Type
Expr → Nat: Type
Nat
| Expr.var_exp: var → Expr
Expr.var_exp (var.mk: Nat → var
var.mk i: Nat
i) => i: Nat
i
| Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp _ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e
| Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp _ e1: Expr
e1 e2: Expr
e2 => max: {α : Type} → [self : Max α] → α → α → α
max (max_variable_index: Expr → Nat
max_variable_index e1: Expr
e1) (max_variable_index: Expr → Nat
max_variable_index e2: Expr
e2)
def num_vars: Expr → Nat
num_vars : Expr: Type
Expr → Nat: Type
Nat := λ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e + 1: Nat
1
Truth Table Output Column
def eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps : List: Type → Type
List Interp: Type
Interp → Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| [], _ => []: List Bool
[]
| h: Interp
h::t: List Interp
t, e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps t: List Interp
t e: Expr
e ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
-- Given expression, return truth table outputs by ascending row index
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps (mk_interps: Nat → List Interp
mk_interps (num_vars: Expr → Nat
num_vars e: Expr
e)) e: Expr
e
Satisfiability Checkers
-- functions to check if bool list has any, resp. all, values true
def reduce_or: List Bool → Bool
reduce_or : List: Type → Type
List Bool: Type
Bool → Bool: Type
Bool
| [] => false: Bool
false
| h: Bool
h::t: List Bool
t => or: Bool → Bool → Bool
or h: Bool
h (reduce_or: List Bool → Bool
reduce_or t: List Bool
t)
def reduce_and: List Bool → Bool
reduce_and : List: Type → Type
List Bool: Type
Bool → Bool: Type
Bool
| [] => true: Bool
true
| h: Bool
h::t: List Bool
t => and: Bool → Bool → Bool
and h: Bool
h (reduce_and: List Bool → Bool
reduce_and t: List Bool
t)
-- Three main functions: test given expression for satsfiability properties
def is_sat: Expr → Bool
is_sat : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => reduce_or: List Bool → Bool
reduce_or (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_valid: Expr → Bool
is_valid : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => reduce_and: List Bool → Bool
reduce_and (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_unsat: Expr → Bool
is_unsat : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => not: Bool → Bool
not (is_sat: Expr → Bool
is_sat e: Expr
e)
Quick Demo
-- some atomic/variable expressions defBread :Bread: ExprExpr := {Expr: Typevar.mkvar.mk: Nat → var0} def0: NatCheese :Cheese: ExprExpr := {Expr: Typevar.mkvar.mk: Nat → var1} def1: NatJam :Jam: ExprExpr := {Expr: Typevar.mkvar.mk: Nat → var2}2: Natis_sat (is_sat: Expr → BoolBread)Bread: Expris_sat (is_sat: Expr → BoolBread ∧ ¬Bread: ExprBread)Bread: Expris_valid (is_valid: Expr → BoolBread ∧ ¬Bread: ExprBread)Bread: Expris_valid (is_valid: Expr → BoolBread ∨ ¬Bread: ExprBread)Bread: Expris_unsat (is_unsat: Expr → BoolBread ∧ ¬Bread: ExprBread)Bread: Expr
UVa CS2120-002 F23 Midterm Exam
The first section of this exam just repeats our definition of propositional logic syntax and semantics. Skip ahead to the second section to find the exam.
Propositional Logic: Syntax, Sematics, Satisfiability
This section of the exam simply includes our formal definition of the syntax and semantics of propositional logic and of functions that determine whether a given expression is valid, satisfiable, or unsatisfiable.
Syntax
-- variables
structure var : Type := (n: Nat)
-- connectives/operators
inductive unary_op : Type | not
inductive binary_op : Type
| and
| or
| imp
| iff
-- expressions (abstract syntax)
inductive Expr : Type
-- Extra credit answers here
| var_exp (v : var)
| un_exp (op : unary_op) (e : Expr)
| bin_exp (op : binary_op) (e1 e2 : Expr)
-- concrete syntax
notation "{"v"}" => Expr.var_exp v
prefix:max "¬" => Expr.un_exp unary_op.not
infixr:35 " ∧ " => Expr.bin_exp binary_op.and
infixr:30 " ∨ " => Expr.bin_exp binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp binary_op.iff
notation " ⊤ " => Expr.top_exp
notation " ⊥ " => Expr.bot_exp
Semantics
-- meanings of unary operators
def eval_un_op : unary_op → (Bool → Bool)
| unary_op.not => not
-- missing binary Boolean operators
def implies : Bool → Bool → Bool
| true, false => false
| _, _ => true
def iff : Bool → Bool → Bool
| true, true => true
| false, false => true
| _, _ => false
-- meanings of binary operators
def eval_bin_op : binary_op → (Bool → Bool → Bool)
| binary_op.and => and
| binary_op.or => or
| binary_op.imp => implies
| binary_op.iff => iff
-- The interpretation type
def Interp := var → Bool
-- The meanings of expressions "under" given interpretations
def eval_expr : Expr → Interp → Bool
-- Extra credit answers here
| (Expr.var_exp v), i => i v
| (Expr.un_exp op e), i => (eval_un_op op) (eval_expr e i)
| (Expr.bin_exp op e1 e2), i => (eval_bin_op op) (eval_expr e1 i) (eval_expr e2 i)
Satisfiability
We built a satisfiability checker for propositional logic, in several pieces. This subsection includes all definitions.
Truth Table Input Rows
-- Nat to Binary -- You don't need to worry about the "have" part def right_bit (n : Nat) := n%2 def shift_right (n : Nat) := n/2 def:Nat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Nat1 => [1: Nat1] |1: Natn' + 2 => have : (n': Natshift_right (shift_right: Nat → Natn' +n': Nat2)) < (2: Natn' +n': Nat2) :=2: Natsorrysorry: shift_right (n' + 2) < n' + 2nat_to_bin (nat_to_bin: Nat → List Natshift_right (shift_right: Nat → Natn' +n': Nat2)) ++ [2: Natright_bit (right_bit: Nat → Natn' +n': Nat2)] -- Left pad with zeros def2: Natzero_pad :zero_pad: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Typev,v: Natl =>l: List Natzero_pad_recursive (zero_pad_recursive: Nat → List Nat → List Natv - (v: Natl.l: List Natlength))length: {α : Type} → List α → Natl wherel: List Natzero_pad_recursive :zero_pad_recursive: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Type0,0: Natl =>l: List Natl |l: List Natv'+1,v': Natl =>l: List Natzero_pad_recursivezero_pad_recursive: Nat → List Nat → List Natv' (v': Nat0::0: Natl) -- Make row of bits at index "row" padded out to "cols" wide defl: List Natmk_bit_row : (mk_bit_row: Nat → Nat → List Natrow:row: NatNat) → (Nat: Typecols :cols: NatNat) →Nat: TypeListList: Type → TypeNat |Nat: Typer,r: Natc =>c: Natzero_padzero_pad: Nat → List Nat → List Natc (c: Natnat_to_binnat_to_bin: Nat → List Natr) -- Convert list of bits to list of bools defr: Natbit_to_bool :bit_to_bool: Nat → BoolNat →Nat: TypeBool |Bool: Type0 =>0: Natfalse | _ =>false: Booltrue deftrue: Boolbit_list_to_bool_list :bit_list_to_bool_list: List Nat → List BoolListList: Type → TypeNat →Nat: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Natt => (t: List Natbit_to_boolbit_to_bool: Nat → Boolh) :: (h: Natbit_list_to_bool_listbit_list_to_bool_list: List Nat → List Boolt) -- Make row'th row of truth table with vars variables deft: List Natmk_row_bools : (mk_row_bools: Nat → Nat → List Boolrow :row: NatNat) → (Nat: Typevars :vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: Typer,r: Natv =>v: Natbit_list_to_bool_list (bit_list_to_bool_list: List Nat → List Boolmk_bit_rowmk_bit_row: Nat → Nat → List Natrr: Natv)v: Nat
Interpretations
-- Convert list of bools to interpretation
def override: Interp → var → Bool → Interp
override : Interp: Type
Interp → var: Type
var → Bool: Type
Bool → Interp: Type
Interp
| old_interp: Interp
old_interp, var: _root_.var
var, new_val: Bool
new_val =>
(λ v: _root_.var
v => if (v: _root_.var
v.n: _root_.var → Nat
n == var: _root_.var
var.n: _root_.var → Nat
n) -- when applied to var
then new_val: Bool
new_val -- return new value
else old_interp: Interp
old_interp v: _root_.var
v) -- else retur old value
def bools_to_interp: List Bool → Interp
bools_to_interp : List: Type → Type
List Bool: Type
Bool → Interp: Type
Interp
| l: List Bool
l => bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper l: List Bool
l.length: {α : Type} → List α → Nat
length l: List Bool
l
where bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper : (vars: Nat
vars : Nat: Type
Nat) → (vals: List Bool
vals : List: Type → Type
List Bool: Type
Bool) → Interp: Type
Interp
| _, [] => (λ _: var
_ => false: Bool
false)
| vars: Nat
vars, h: Bool
h::t: List Bool
t =>
let len: Nat
len := (h: Bool
h::t: List Bool
t).length: {α : Type} → List α → Nat
length
override: Interp → var → Bool → Interp
override (bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper vars: Nat
vars t: List Bool
t) (var.mk: Nat → var
var.mk (vars: Nat
vars - len: Nat
len)) h: Bool
h
-- Make an interpretation for given row with "vars" variables
def mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row : (vars: Nat
vars: Nat: Type
Nat) → (row: Nat
row: Nat: Type
Nat) → Interp: Type
Interp
| v: Nat
v, r: Nat
r => bools_to_interp: List Bool → Interp
bools_to_interp (mk_row_bools: Nat → Nat → List Bool
mk_row_bools r: Nat
r v: Nat
v)
-- Given number of variables, return list of interpretations
def mk_interps: Nat → List Interp
mk_interps (vars: Nat
vars : Nat: Type
Nat) : List: Type → Type
List Interp: Type
Interp :=
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper (2: Nat
2^vars: Nat
vars) vars: Nat
vars
where mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper : (rows: Nat
rows : Nat: Type
Nat) → (vars: Nat
vars : Nat: Type
Nat) → List: Type → Type
List Interp: Type
Interp
| 0: Nat
0, _ => []: List Interp
[]
| (n': Nat
n' + 1), v: Nat
v => (mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row v: Nat
v n': Nat
n')::mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper n': Nat
n' v: Nat
v
-- Count the number of variables in a given expression
def max_variable_index: Expr → Nat
max_variable_index : Expr: Type
Expr → Nat: Type
Nat
| -- Extra credit answers here
| Expr.var_exp (var.mk i) => i
| Expr.un_exp _ e => max_variable_index e
| Expr.bin_exp _ e1 e2 => max (max_variable_index e1) (max_variable_index e2)
def num_vars: Expr → Nat
num_vars : Expr: Type
Expr → Nat: Type
Nat := λ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e + 1: Nat
1
Truth Table Output Column
def eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps : List: Type → Type
List Interp: Type
Interp → Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| [], _ => []: List Bool
[]
| h: Interp
h::t: List Interp
t, e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps t: List Interp
t e: Expr
e ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
-- Given expression, return truth table outputs by ascending row index
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps (mk_interps: Nat → List Interp
mk_interps (num_vars: Expr → Nat
num_vars e: Expr
e)) e: Expr
e
Reducers: Boolean List to Bool with And and Or
-- functions to check if bool list has any, resp. all, values true
def reduce_or: List Bool → Bool
reduce_or : List: Type → Type
List Bool: Type
Bool → Bool: Type
Bool
| [] => false: Bool
false
| h: Bool
h::t: List Bool
t => or: Bool → Bool → Bool
or h: Bool
h (reduce_or: List Bool → Bool
reduce_or t: List Bool
t)
def reduce_and: List Bool → Bool
reduce_and : List: Type → Type
List Bool: Type
Bool → Bool: Type
Bool
| [] => true: Bool
true
| h: Bool
h::t: List Bool
t => and: Bool → Bool → Bool
and h: Bool
h (reduce_and: List Bool → Bool
reduce_and t: List Bool
t)
Satisfiability Checkers
-- Three main functions: test given expression for satsfiability properties
def is_sat: Expr → Bool
is_sat : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => reduce_or: List Bool → Bool
reduce_or (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_valid: Expr → Bool
is_valid : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => reduce_and: List Bool → Bool
reduce_and (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_unsat: Expr → Bool
is_unsat : Expr: Type
Expr → Bool: Type
Bool := λ e: Expr
e : Expr: Type
Expr => not: Bool → Bool
not (is_sat: Expr → Bool
is_sat e: Expr
e)
EXAM STARTS HERE
#1 Proofs as Programs
a. And elimination [15 points]
Prove, by completing the following function definition, that from a value of type α × β one can always derive a value of type α.
-- Your answer here defand_elimination {and_elimination: {α β : Type} → α × β → ααα: Typeβ :β: TypeType} :Type: Type 1α ×α: Typeβ →β: Typeα | (_, _) =>α: Type
b. Funny transitivity [15 points]
Prove, by completing the following function definition, that (α → β) × (β → γ) → (α → γ). In other words, if you have a pair of functions, one converting α to β and one converting β to γ, then you can always construct a function from α to γ. Hint: Use type-guided top-down programming, and remember how to express a function value: that's what you have to return in this case.
-- Your answer here def funny_transitivity {α β γ : Type} : (α → β) × (β → γ) → (α → γ) | _ =>
c. Ex empty quodlibet [15 points]
Prove that if a type, α, is uninhabited then from an assumed value (a : α) one can always derive a value of any type, β.
-- Your answer here def ex_empty {α β : Type} : (α → Empty) → α → β | _, _ =>
#2 Data Types
a. Enumerated Types [10 points]
Define three enumerated types, called Bread, Spread, and Cheese, where the values of type Bread are white and wheat; the values of type Spread are jam and pesto; and the values of type Cheese are cheddar and brie.
-- Your answers here
b. An interesting inductive type [15 points]
Define a data type called Sandwich, with one constructor called mk, taking as its arguments a choice of bread and either (but not both) a choice of Cheese or a choice of Spread. Hint: Remember how to define a type that carries a value of either one type or another. Extra credit [2pts] for using structure instead of inductive to declare the Sandwich type.
-- Your answer here
c. Now make yourself a Sandwich [15 points]
Define jam_sandwhich to be a Sandwhich made with wheat bread and jam. You have to use Sandwich.mk to create a term representing a sandwich with wheat bread and jam as a spread.
-- Your answer here def jam_sandwich : Sandwich :=
#3 Recursive Data and Functions [15 points]
In our implementation of propositional logic, we defined a function, bit_list_to_bool_list, to convert a list of Nat to a corresponding list of Bool. Here's the definition (with a tick mark on the name).
defbit_list_to_bool_list' :bit_list_to_bool_list': List Nat → List BoolListList: Type → TypeNat →Nat: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Natt => (t: List Natbit_to_boolbit_to_bool: Nat → Boolh) :: (h: Natbit_list_to_bool_listbit_list_to_bool_list: List Nat → List Boolt) -- expect [true, false, true, false, true]t: List Natbit_list_to_bool_list [bit_list_to_bool_list: List Nat → List Bool1,1: Nat0,0: Nat3,3: Nat0,0: Nat1]1: Nat
Your job is to generalize this solution by defining a new function, called map. Generalize the types, Nat and Bool, to arbitrary types, α and β. Generalize bit_to_bool to be any function for converting an individual α value into a corresponding β value. Your map function will thus take as its arguments (1) type parameters (make them implicit), (2) a function for converting elements, and (3) a List of α values, and will return a correspond List of β values.
-- Your answer here -- test case: use map instead of bit_list_to_bool_list -- expect [true, false, true, false, true] #evalbit_to_bool [1, 0, 1, 0, 1]bit_to_bool [1, 0, 1, 0, 1]: ?m.44293
#4 Propositional Logic [10 pts Extra Credit, for A+]
Propositional logic as we've formulate it has variable expressions (atomic expressions), and larger expressions built by applying connectives (∧, ∨, ¬, ⇒, ⇔) to smaller expressions.
Some formalizations of propositional logic also include the constant expressions True and False. In concrete syntax, they are sometimes written as ⊤ (pronounced top) and ⊥ (bottom). Semantically ⊤ evaluates to Boolean true and ⊥ evaluates to Boolean false.
a. Extend Syntax and Semantics
Your job is to extend our syntax and semantics to include ⊤ and ⊥ as valid expressions. You will have to carry out the following tasks.
- add top_exp and bot_exp as constructors in Expr
- note that we've already added concrete notation definitions
- add rules for evaluating these expressions to eval_expr
- add rules for these expressions to max_variable_index
When you're done, the following logic should evaluate without error.
defX := {X: Exprvar.mkvar.mk: Nat → var0}0: Nat-- expect true
b. Give a model for (X ⇒ ⊥)
Recall that a model is a binding of values to variables that makes a proposition true. What value must X have to make (X ⇒ ⊥) true?
-- Answer: {X is _____ } is a model of (X ⇒ ⊥)
Final #1 : Binary Relations and Equality
This question will test your ability to build on what you have already learned. You are given a lesson on how binary relations (which you can think of as sets of pairs of values) are specified by two-place predicates along with rules for forming proofs of such predicates.
Representing Relations as Two-Place Predicates
We've seen how to represent sets, in set theory, as logical predicates, and set operations as logical operations on underlying predicates. For example set union is disjunction of respective membership predicates. Different sets of objects of some type, α, are specified by different predicates, all of one argment: of type α → Prop in Lean's type theory.
To represent not a set but a binary relation, r, on objects of some type, α, we use predicates with two arguments: r : α → α → Prop. You can think of a relation as specifying a set of pairs of values: those pairs that satisfy the given predicate. As an example, r could be the less than or the equals relation on natural numbers.
Now we will often want to define our own types, 2-place predicates on values of such types, and axioms for constructing membership proofs.
Example: Defining a Binary likes Relation on Dogs
Let's consider a simple example. We'll define a type called Dog; three dogs of this type (rover, fido, and iris); a binary likes relation on dogs; and, finally, we'll specify what proofs can be constructed: that rover likes fido, fido likes iris, and iris likes rover.
Inductive Families of Propositions and Proofs
In Lean we can write such a specification in the form of what we call an inductive family of propositions and associated proofs, as follows. The name of the relation (defined next) is likes. It takes two dog arguments and yields a proposition that we read as asserting that the first dog likes the second.
Here's our dog type for purposes of this example. With this type in hand, we'll be able to define a binary relation on these dogs.
inductive dog: Type
dog : Type: Type 1
Type
| rover: dog
rover
| fido: dog
fido
| iris: dog
iris
open dog
Here's the important new idea: we specify a family of likes propositions using inductive, and the set of associated proofs are given by the constructors of this type. Namely, we can form a proposition that any dog likes any other, but the only proofs we have, and thus the only pairs of dogs that are in the relation are (rover, fido), (fido, iris), and (iris rover).
inductive likes: dog → dog → Prop
likes : dog: Type
dog → dog: Type
dog → Prop: Type
Prop
| rlf: likes rover fido
rlf : likes: dog → dog → Prop
likes rover: dog
rover fido: dog
fido
| fli: likes fido iris
fli : likes: dog → dog → Prop
likes fido: dog
fido iris: dog
iris
| ilr: likes iris rover
ilr : likes: dog → dog → Prop
likes iris: dog
iris rover: dog
rover
open likes
We can now form any likes proposition using this proposition builder.
likeslikes: dog → dog → Propirisiris: dogfidofido: doglikeslikes: dog → dog → Propirisiris: dogroverrover: doglikeslikes: dog → dog → Propirisiris: dogirisiris: dog
Moreover, we can state and prove propositions about dogs liking each other.
example :example: likes rover irislikeslikes: dog → dog → Proproverrover: dogiris :=iris: dog-- oops, no proof of this example : ¬ likes rover iris := λ h => nomatch h -- ok that's provable example : likes iris rover := ilr -- yep, there's a proof example : likes rover fido ∧ likes iris rover := ⟨ rlf, ilr ⟩ --true!
Example: A Relation Associating Each Nat With Its Square
Inductive families can be used to formalize all manner of important and interesting relations. Here's another example were we define a relation that associates each natural number with its square.
inductive square : Nat → Nat → Prop
| sqr (a b : Nat) : b = a ^ 2 → square a b
open square
You can read this definition as saying that square is a binary relation on natural numbers (first line), and (constructor). It is applied to any two natural numbers, a and b, along with a proof that b = a ^ 2 to construct a proof of square a b.
def sqr11 : square 1 1 := sqr 1 1 rfl
def sqr39 : square 3 9 := sqr 3 9 rfl
example : ¬ square 3 8 := λ h => nomatch h -- proof by negation
example : square 1 1 ∧ square 3 9 := ⟨ sqr11, sqr39 ⟩
Exam Question #1
Express the following proposition in English, complete the formal proof of it, and briefly explain in English how you proved it.
example : ∃ n, square n 4 := Exists.intro 2 () -- fill in _ /- English language translation of propostion here: -/
The Binary Equality Relation on Objects of a Type α
With these concepts under our belts we can now understand equality as a binary relation. In fact, in Lean, it's a polymorphic binary relation. Given any type, α, there is a binary equality relation on values of type α. Equality is defined as a polymorphic inductive family, called Eq with one type parameter, two value parameters, and one proof constructor, called refl. We'll unpack all of this in steps.
The Inductive Family of Propositions and Proofs
First, let's first see how applying Eq to a type, α, yields the type of binary binary relations, represented as two-argument predicates, on α: that is, as a function that takes two arguments of type α and that returns the proposition that they're equal. (NB: We're not yet talking about proofs.)
-- Polymorphic Equality Relation Builder@Eq -- @Eq : {α : Sort u} → α → α → Prop -- Sort u means any type -- The type of binary relations on the natural numbersEq: {α : Sort u_1} → α → α → Prop@EqEq: {α : Type} → α → α → PropNat -- Eq : Nat → Nat → Prop -- The type of binary relation on stringsNat: Type@EqEq: {α : Type} → α → α → PropString -- Eq : String → String → PropString: Type
Next Let's see what propositions result when we apply Eq to a type, α, and two values of that type. We expect a proposition that the two values are equal, and that's exactly what we get.
@EqEq: {α : Type} → α → α → PropNatNat: Type11: Nat2 -- The proposition that 1 = 2 -- We can omit the @ and let Lean infer the type argument2: NatEqEq: {α : Type} → α → α → Prop11: Nat2 -- a false equality proposition2: NatEqEq: {α : Type} → α → α → Prop00: Nat0 -- a true equality proposition0: NatEqEq: {α : Type} → α → α → Prop"Hi""Hi": String"Bob" -- a false equality of strings"Bob": String
Infix Notation: = is Eq
Now note: a = b is just infix notation for Eq a b! In the first of the following definitions, we use Eq as a prefix notation. In the second, we use = as infix notation. To see how to prove an equality proposition, just look at the proof construction rule defined for Eq.
example: ∀ (α : Type) (a : α), a = a
example (α: Type
α : Type: Type 1
Type): ∀ (a: α
a : α: Type
α), Eq: {α : Type} → α → α → Prop
Eq a: α
a a: α
a := λ a: α
a => Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl a: α
a
example: ∀ (α : Type) (a : α), a = a
example (α: Type
α : Type: Type 1
Type): ∀ (a: α
a : α: Type
α), a: α
a = a: α
a := λ a: α
a => Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl a: α
a
Note that it's a type error in Lean to ask about equality of objects of different types. In the following example, Lean complains not that the types are different but that it tried and failed to convert the first argument, 1, to a corresponding String value. Lean natively lacks a rule for performing such coercions.
EqEq: {α : Type} → α → α → Prop"Hi""Hi": String
Formal Definition of Equality Relation(s) in Lean
So we are now finally ready to meet Lean's definition of the equality relation, Eq, including the means by which one constructs proofs of equality. Here it is, from mathlib (Lean's library of mathematical definitions). We put a ' on the name so as not to clash with Lean's definition.
inductive Eq': {α : Sort u_1} → α → α → Prop
Eq' : α: Sort u_1
α → α: Sort u_1
α → Prop: Type
Prop where
| refl: ∀ {α : Sort u_1} (a : α), Eq' a a
refl (a: α
a : α: Sort u_1
α) : Eq': {α : Sort u_1} → α → α → Prop
Eq' a: α
a a: α
a
Let's dissect this definition. We'll use Lean's definition henceforth. First, we note that the first line uses α without declaring it. Lean 4 is programmed to assume that α is some type. The first line is thus equivalent to the following: inductive Eq (α : Sort u): α → α → Prop.
Eq'Eq': {α : Type} → α → α → Prop22: Nat3 -- Using our definition of Eq'3: NatEqEq: {α : Type} → α → α → Prop22: Nat3 -- Lean supports infix = notation3: Nat
The Introduction Rule for Eq
And what about proofs? How can we prove 1 = 1, for example?
Eq provides a single constructor, refl, with any value, a : α, as an argument. The term, Eq.refl a is defined by this rule to be a proof of a = a. Eq.refl is thus the introduction rule for equality: the rule for constructing a proof an equality. Indeed, it is the only way to create a proof of an equality proposition.
Note that there is no way to form a proof of a = b for different values of a and b, as refl takes only one argument! One thing to note is that Lean reduces terms, such as a and b, when Eq is applied, so Eq.refl will prove a = b if they are the same term when reduced. This allows us to prove equality propositions such as 2 = 1 + 1, because both sides reduce to the same term, 2. Let's assert some equalities and see what we can prove.
example: 1 = 1
example : 1: Nat
1 = 1: Nat
1 := Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl 1: Nat
1
example: 1 = 1
example : 1: Nat
1 = 1: Nat
1 := rfl: ∀ {α : Type} {a : α}, a = a
rfl -- Lean infers α = Nat *and* a = 1
example: 2 = 1 + 1
example : 2: Nat
2 = 1: Nat
1 + 1: Nat
1 := rfl: ∀ {α : Type} {a : α}, a = a
rfl -- Lean reduces 1 + 1 to 2, rfl works
example: "Hi" = "Hi"
example : "Hi": String
"Hi" = "Hi": String
"Hi" := Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl "Hi": String
"Hi"
example: true = (true || false)
example : true: Bool
true = or: Bool → Bool → Bool
or true: Bool
true false: Bool
false := Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl true: Bool
true
-- Three proofs of the same inequality: "proof by negation"
example: 1 ≠ 2
example : 1: Nat
1 ≠ 2: Nat
2 := λ h: 1 = 2
h => nomatch h: 1 = 2
h
example: ¬1 = 2
example : ¬1: Nat
1 = 2: Nat
2 := λ h: 1 = 2
h => nomatch h: 1 = 2
h
example: 1 = 2 → False
example : 1: Nat
1 = 2: Nat
2 → False: Prop
False := λ h: 1 = 2
h => nomatch h: 1 = 2
h
What About rfl?
Ok, so what about this rfl thing we've been using to produce proofs of equality? It's just function that applies Eq.refl to α and a, but now both values are inferred from context. The key difference between rfl and Eq.refl is that rfl takes both the type and the value argument implicitly, whereas refl requires that the value, a, be given explicitly. Both operations return a proof of a = a for any a of any type. The rfl function does so by using Eq.refl. If Lean cannot infer a due to lack of context, you have to use Eq.refl and give a explicitly.
Here you can see that rfl really just infers a and applies Eq.refl to it.
@rfl -- fun {α} {a} => Eq.refl arfl: ∀ {α : Sort u_1} {a : α}, a = aexample :example: 1 = 11 =1: Nat1 :=1: Natrfl -- infers α = Nat, a = 1rfl: ∀ {α : Type} {a : α}, a = aexample :example: 1 = 11 =1: Nat1 :=1: NatEq.reflEq.refl: ∀ {α : Type} (a : α), a = a1 -- infers α = Nat, a = 11: Nat
The Elimination Rule for Equaltiy
We've now seen the introduction rule, refl, for constructing proofs of equality. What is or are the elimination rules? If you have a proof of equality, how can you use it in constructing other proofs? The answer is if you know (have a proof, h : a = b), and you have a proof, pa : P a, of a proposition, P a (formed by applying a predicate P to a), then you can use h : a = b to rewrite your proof of P a into a proof of P b.
Here's an example. If you know Mary is Nice, and Mary is also know as (is equal to) Maire, then you can conclude that Maire is Nice. In other words, if a = b, then you can substitute b for a in any proposition without changing its meaning. Again, informally, if P a (Mary is Nice), and a = b (Mary also known as Maire), then P b (Maire is nice).
This rule, known as the substitutivity of equals, is the elimination ("how to use") rule for proofs of equalities. In Lean it's called Eq.subst, and it's defined as follows. Note that in this definition, motive is just another name for P in our previous example.
@Eq.substEq.subst: ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive b
Eq.subst.{u} {α : Sort u} {motive : α → Prop} {a b : α} (h₁ : a = b) (h₂ : motive a) : motive b
In other words, given (1) any type of values, α, (2) any predicate, motive, on values of this type, (3) two values a and b, (4) a proof of a = b, and (5) a proof of motive a (that a satisfies the predicate), then you can by applying this rule obtain a proof that b satisfies it, too. Let's see some examples. Note that the type, predicate, and two values are all inferred, so in a practical application of Eq.subst you only provide a proof of equality and a proof of motive a to get a proof of motive b. (In our example, again, motive is the Nice predicate.)
section variable (Person :Person: TypeType) (Type: Type 1MaryMary: PersonMaire :Maire: PersonPerson) (Person: TypeNice :Nice: Person → PropPerson →Person: TypeProp) (Prop: Typeh₁ :h₁: Mary = MaireMary =Mary: PersonMaire) (Maire: Personh₂ :h₂: Nice MaryNiceNice: Person → PropMary)Mary: Person@Eq.substEq.subst: ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive bexample :example: ∀ (Person : Type) (Mary Maire : Person) (Nice : Person → Prop), Mary = Maire → Nice Mary → Nice MaireNiceNice: Person → PropMaire :=Maire: PersonEq.substEq.subst: ∀ {α : Type} {motive : α → Prop} {a b : α}, a = b → motive a → motive bh₁h₁: Mary = Maireh₂ endh₂: Nice Mary
Understand this example! From Mary = Maire and Mary is nice we proved that Maire is nice, and the proof was by substitution of equals for equals, namely Maire for Mary. That's how you'd explain it in English.
Here's another example. It formalizes just what we've said. If α is any type; P is a predicate on values of this type; a and b are values of this type, you have a proof, h, of a = b, and you have a proof, pa : P a, then you can have a proof of P b by applying Eq.subst.
example: ∀ {α : Type} {P : α → Prop} {a b : α}, a = b → P a → P b
example {α: Type
α : Type: Type 1
Type} {P: α → Prop
P : α: Type
α → Prop: Type
Prop} {a: α
a b: α
b : α: Type
α} (h: a = b
h : a: α
a = b: α
b) (pa: P a
pa : P: α → Prop
P a: α
a) : P: α → Prop
P b: α
b :=
Eq.subst: ∀ {α : Type} {motive : α → Prop} {a b : α}, a = b → motive a → motive b
Eq.subst h: a = b
h pa: P a
pa
Aside on Tactics in Lean
In Lean, applying Eq.subst directly can be tricky and doesn't always produce what you want. The reasons are beyond the scope of this class. The upshot is that for us it's better to use a predefined Lean tactic, rw (short for rewrite), that does the work of applying Eq.subst for us.
A tactic in Lean is a proof constructing automation. When providing a term of any type, you cal always switch into tactic mode using the keyword, by, that is followed then by a sequence of tactic applications.
Tactics can take arguments. The rw tactic is given to us from Lean's library. Applied with a proof of an equality, h : a = b, rw [h] rewrites each a in the current goal to b. That's logically sound: after all a = b. This is very helpful if you have a proof of P b. To do a rewrite in the reverse direction, rewriting each b in the current goal to a, justified by the same equality, proof, h : a = b, one writes rw [←h]. Here's an example. Study carefully the starting context, understand how the rewrite should work, and confirm it does.
Place your cursorat the end of the by line, then after each of the tactic lines, so see how the goal and your context change when you apply the rw tactics.
example {example: ∀ {α : Type} {P : α → Prop} {a b : α}, a = b → P a → P bα :α: TypeType} {Type: Type 1P :P: α → Propα →α: TypeProp} {Prop: Typeaa: αb :b: αα} (α: Typeh :h: a = ba =a: αb) (b: αpa :pa: P aPP: α → Propa):a: αPP: α → Propb :=b: α-- use tactic mode to construct required termGoals accomplished! 🐙α: Type
P: α → Prop
a, b: α
h: a = b
pa: P aP bα: Type
P: α → Prop
a, b: α
h: a = b
pa: P aP a-- rewrite b to a in the goalα: Type
P: α → Prop
a, b: α
h: a = b
pa: P aP a-- we already have a proof of the goal; doneGoals accomplished! 🐙
Properties of the Equality Relation
Properties of the Equality Relation
From just the introduction rule (reflexivity) and the elimination rule (substitutability of equals for equals) we can prove that the equality relation has three critical additional properties:
- it's symmetric
- it's transitive
- it's an equivalence relation
Here we will focus on symmetry and transitivity. Being an equivalence relation just means that it reflexive, symmetric, and transitive. We will explain how to prove that equality is symmetric. We will then leave it to you to prove that it is also transitive. That will show that you are conversant with the ideas from this class and from this problem.
Equality is Reflexive
Equality is reflexive by definition: for any (a : α), the term, Eq.refl a, is accepted as a proof of a = a. So if you need a proof of a = a, you can, and indeed must, construct it by applying the Eq.refl *proof constructor to a.
Here's another proof of reflexivity. Of course is just boils down to the refl constructor. It makes Eq reflexive by definition.
See the following code. We hypothesize that α is any type and a is any value of this type. We then prove the proposition that a = a. Be sure to read this example and understand it fully.
example (α : Type): ∀ (a : α), a = a := λ a => Eq.refl a
Theorem: Equality is Symmetric
Our second example is a statement and proof of that the binary equality relation on a type of objects is symmetric.
theorem eq_is_symm {α : Type} {a b : α} : a = b → b = a := λ h =>-- Put cursor here to see contextGoals accomplished! 🐙α: Type
a, b: α
h: a = bb = aα: Type
a, b: α
h: a = bb = bGoals accomplished! 🐙
It's important to put your see the context after we assume h but before we do the rewrite. Observe that the rewrite will write left terms into right terms in the current goal. That makes the goal into b = b. And that is true with a proof by rfl.
Exam Question #2: State and Prove that Equality is Transitive
Formally state and prove that equality is transitive. Finish the proposition (fill in the first underscore "hole") so that it asserts that equality is transitive. Avoid using ∧ in your definition; use → instead. It simplifies the proofs.
Model your answer on the example of symmetry above. You already know how to construct proofs of implications. The new element here is rewriting goals based on known (or assumed) equalities.
Notes: (1) fill in the _ holes. (2) you can and will have to write separate tactic applications indented on separate lines.
theorem eq_rel_trans: {α : Type} → {a b c : α} → (x : ?m.2325) → (x_1 : ?m.2326 x) → ?m.2327 x x_1
eq_rel_trans {α: Type
α : Type: Type 1
Type} {a: α
a b: α
b c: α
c : α: Type
α} :
_: Sort (imax u_1 u_2 u_3)
_ -- fill with proposition: equality is transitive
| _, _ => by
_ -- fill in your proof of it here
Exam Question #3
You know that the Nat.succ function takes any natural number and returns its successor. You are to define the corresponding binary relation, as an inductive family. It will specify a set of pairs of natural number values, (a, sa), where sa = Nat.succ a. Then prove (1) the pair, (2, 3) is in the relation, and the pair (2,4) is not.
-- Your answer here-- import Mathlib.Data.Set.Basic
Final Exam: Part 2
Here you can show that you've got what we covered in class, up to and including set theory. The focus is on set theory, as it encompassess the underlying logic as well. A hint: You will want to review the existential quantifier and proofs of existentially quantified propositions.
Problem #1:
Use set comprehension notation in Lean to define odds as the set of odd numbers, by way of a membership predicate for this set.
-- Here
Problem #2:
Use set comprehension and other set notations in Lean to define the set, perfect_squares, of natural numbers, n, such that each n is the square of some natural number, m. For example, 36 is a perfect square because it is the square of another number, namely m = 6.
-- Here
Problem #3:
Use set comprehension notation to define the set, odd_perfects, to be the intersection of the odds and the perfect squares.
-- Here
Problem #4:
Formally state and prove the proposition that 9 ∈ odd_perfects. Hint: A proof within a proof.
-- HereExternal Lean Resources
Here are links to external resources for Lean4. They are not suitable for regular use in this class, as they mostly assume considerable background knowledge, but I provide them as you might occasionally find them useful, and if you're really interested in what we cover in this class, by all means dive in to the world of dependable and automated reasoning.
- Using the Lean VSCode extension
- Lean4 Tactics for Beginners
- Lean Manual
- Theorem Proving in Lean
- Advanced tutorial slides
CS2120 Fall 2023 Section 002 (Sullivan)
Welcome to UVa CS2120 Fall 2022. Here are the instructions for setting up your logic and proof systems for this semester.
Detailed installation instructions
- Update your operating system:
- If MacOS: Be sure your OS is up-to-date (really).
- If Windows: You must be running Windows Pro, Education, Enterprise, or 11. If you're running Windows 10, update to Windows 11.
- Install git on your computer (if you know you already have it, skip this step):
- Windows: https://git-scm.com/download/win
- OSX/MacOs
- Find and run the Terminal program
- Enter the following command in the window: xcode-select --install
- When it asks, please go ahead with the standard install process.
- Have a GitHub account. Create one for yourself if necessary. It's free: https://github.com/
- Install Docker Desktop: https://www.docker.com/products/docker-desktop. It's free. If you already have it, update it to the current version.
- Install VSCode: https://code.visualstudio.com/download. It's free.
- Launch Docker Desktop and watch for it to complete its start-up procedures.
- Use GitHub to fork this repository:
- Be logged in to your GitHub account.
- Visit this repository on GitHub (which is probably where you're reading this) while logged in to your GitHub account.
- "Fork" this repo using the Fork button in the upper right corner. This will create a clone (copy) of this repository under your GitHub account.
- Visit your GitHub page to confirm that you now own a clone of this repository. Click to view the repository.
- Select the green Code button, then HTTPS, then copy the provided URL. This is the GitHub URL of your newly forked copy of the respository.
- Start up your new environment:
- Start a new VSCode window.
- Using the "extensions" tool (four squares with one out of place on the left), search for and install the Remote Development extension.
- Use CTRL/CMD-SHIFT-P to bring up the VSCode command palatte.
- Search for and select Clone Repository in Container Volume
- Paste in the GitHub URL that you copied above into the input box.
- If you're asked to choose something, select unique repository.
- Now wait while your environment is built. It will take a while, possibly 15 minutes or more.
- You can click the Starting Dev Container to see the build process if you want. Wait for the building activity to end and for your environment to "boot up" before taking any further actions. There is a status bar at the bottom of the screen that reflects build processes status and activities.
- Configure git on your new containerized operating system
- Open a new Terminal window in VSCode
- Issue the following commands, filling in your details as appropriate
- git config --global user.name "Your Name Here"
- git config --global user.email "your@email.here"
- You may now work in and exit from VSCode as you wish. VSCode will let you re-open this project when you're ready to work on it again.
You now have, up and running, a nice discrete math development environment. You're done here now!
- Acknowledgement: This work is supported in part by the National Science Foundation under grant (Award Abstract) #1909414.
- Copyright: © 2021-2023 by Kevin Sullivan
- Contact Author: Kevin Sullivan. sullivan@virginia.edu.