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 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.

Bool : Type
Bool: Type
Bool
Nat : Type
Nat: Type
Nat
String : Type
String: Type
String

Here are some terms (values) of these types. Every term in Lean has a type. #check tells you the type of any term.

Bool.true : Bool
true: Bool
true
-- "literal" term of type Bool
Bool.false : Bool
false: Bool
false
-- another one
true && false : Bool
(
and: Bool → Bool → Bool
and
true: Bool
true
false: Bool
false
) -- a function application term
true && false : Bool
(
true: Bool
true
&&
false: Bool
false
) -- using "infix" notation for "and"

Some terms of type Nat (for "natural number")

0 : Nat
0: Nat
0
1 : Nat
1: Nat
1
2 : Nat
2: Nat
2

Some terms of type String

"" : String
"": String
""
"Logic is the best!" : String
"Logic is the best!": String
"Logic is the best!"
String.append "I love DM1" "!" : String
(
String.append: String → String → String
String.append
"I love DM1": String
"I love DM1"
"!": String
"!"
)
"I love DM1" ++ "!" : String
(
"I love DM1": String
"I love DM1"
++
"!": 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 : Type
Bool: Type
Bool
Bool: Type
Bool

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 Bool : Type
Bool: Type
Bool
Bool: Type
Bool
Bool: Type
Bool

Here's the type of function that takes a natural number and that returns a natural number.

Nat Nat : Type
Nat: Type
Nat
Nat: Type
Nat

This is the type of any function that takes two natural numbers and returns a natural number as a result.

Nat Nat Nat : Type
Nat: Type
Nat
Nat: Type
Nat
Nat: Type
Nat

A function of this type takes two string arguments and returns a string result.

String String String : Type
String: Type
String
String: Type
String
String: Type
String

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 String : Type
String: Type
String
(
String: Type
String
String: Type
String
)

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) String String : Type
(
String: Type
String
String: Type
String
)
String: Type
String
String: Type
String

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 : Bool Bool
(
not: Bool → Bool
not
) -- Boolean negation
and : Bool Bool Bool
(
and: Bool → Bool → Bool
and
) -- Boolean and (&&)
String.append : String String String
(
String.append: String → String → String
String.append
) -- Appends two strings
Nat.succ : Nat Nat
(
Nat.succ: Nat → Nat
Nat.succ
) -- Adds 1 to its argument
Nat.add : Nat Nat Nat
(
Nat.add: Nat → Nat → Nat
Nat.add
) -- Returns sum of two natural numbers

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.

s1 : String
s1: String
s1
s2 : String
s2: String
s2

Evaluating an identifier returns its value

Evaluating a name yields the result of evaluating the term to which it is bound.

"Hello, "
s1: String
s1
-- "Hello, "
"Lean!"
s2: String
s2
-- "Lean!"
"Hello, Lean!"
(
String.append: String → String → String
String.append
s1: String
s1
s2: String
s2
) -- "Hello, Lean!"

Identifiers can be passed as arguments

We can use names to pass terms to functions, as we just saw. Here it is again.

"Hello, Lean!"
String.append: String → String → String
String.append
s1: String
s1
s2: String
s2

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.

def 
s3: String
s3
:=
String.append: String → String → String
String.append
s1: String
s1
s2: String
s2
s3 : String
s3: String
s3
"Hello, Lean!"
s3: String
s3

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.

"Hello, Lean!"
String.append: String → String → String
String.append
"Hello, ": String
"Hello, "
"Lean!": String
"Lean!"
"Hello, Lean!"
(
String.append: String → String → String
String.append
"Hello, ": String
"Hello, "
)
"Lean!": String
"Lean!"

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, ".

def 
f1: String → String
f1
:=
String.append: String → String → String
String.append
s1: String
s1
-- "Hello, " is baked in to f1
f1 : String String
(
f1: String → String
f1
) -- f1 is a function of type 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.

"Hello, Lean!"
f1: String → String
f1
"Lean!": String
"Lean!"
-- "Hello, Lean!"
"Hello, Mary!"
f1: String → String
f1
"Mary!": String
"Mary!"
-- "Hello, Mary!"
"Hello, Joe!"
f1: String → String
f1
"Joe!": String
"Joe!"
-- "Hello, Joe"

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.

7
Nat.add: Nat → Nat → Nat
Nat.add
2: Nat
2
5: Nat
5
-- 7 def
add2: Nat → Nat
add2
:=
Nat.add: Nat → Nat → Nat
Nat.add
2: Nat
2
-- a function that adds 2 to any Nat!
7
add2: Nat → Nat
add2
5: Nat
5
-- 7
12
add2: Nat → Nat
add2
10: Nat
10
-- 12
17
add2: Nat → Nat
add2
15: Nat
15
-- 17

A self-test

To see if you've gotten it, consider these three function types and answer the following questions.

(String String) String : Type
(
String: Type
String
String: Type
String
)
String: Type
String
-- #1
String String String : Type
String: Type
String
(
String: Type
String
String: Type
String
) -- #2
String String String : Type
String: Type
String
String: Type
String
String: Type
String
-- #3

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 String
(
crazy: (String → String) → String → String
crazy
) -- (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.

"Hello, Hello, "
crazy: (String → String) → String → String
crazy
f1: String → String
f1
s1: String
s1
-- Results in application of f1 to s1

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!".

"Hello, Lean!"
crazy2: (String → String) → String → String
crazy2
f1: String → String
f1
s2: String
s2

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 : Bool Bool
(
not: Bool → Bool
not
)
and : Bool Bool Bool
(
and: Bool → Bool → Bool
and
)
or : Bool Bool Bool
(
or: Bool → Bool → Bool
or
)

We can confirm that these functions behave as expected

!true : Bool
not: Bool → Bool
not
true: Bool
true
-- false
!false : Bool
not: Bool → Bool
not
false: Bool
false
-- true
true && true : Bool
and: Bool → Bool → Bool
and
true: Bool
true
true: Bool
true
-- true
true && false : Bool
and: Bool → Bool → Bool
and
true: Bool
true
false: Bool
false
-- false
false && true : Bool
and: Bool → Bool → Bool
and
false: Bool
false
true: Bool
true
-- false
false && false : Bool
and: Bool → Bool → Bool
and
false: Bool
false
false: Bool
false
-- false
true || true : Bool
or: Bool → Bool → Bool
or
true: Bool
true
true: Bool
true
-- true
true || false : Bool
or: Bool → Bool → Bool
or
true: Bool
true
false: Bool
false
-- true
false || true : Bool
or: Bool → Bool → Bool
or
false: Bool
false
true: Bool
true
-- true
false || false : Bool
or: Bool → Bool → Bool
or
false: Bool
false
false: Bool
false
-- false

Not all Boolean functions are built-in

But xor, nor, and nand are not defined

#check (
Error: unknown identifier 'xor'
) #check (
Error: unknown identifier 'nand'
) #check (
Error: unknown identifier 'nor'
)

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.

def 
xor: Bool → Bool → Bool
xor
:
Bool: Type
Bool
->
Bool: Type
Bool
->
Bool: Type
Bool
|
true: Bool
true
,
true: Bool
true
=>
false: Bool
false
|
true: Bool
true
,
false: Bool
false
=>
true: Bool
true
|
false: Bool
false
,
true: Bool
true
=>
true: Bool
true
|
false: Bool
false
,
false: Bool
false
=>
false: Bool
false
false
xor: Bool → Bool → Bool
xor
true: Bool
true
true: Bool
true
-- false
true
xor: Bool → Bool → Bool
xor
true: Bool
true
false: Bool
false
-- true
true
xor: Bool → Bool → Bool
xor
false: Bool
false
true: Bool
true
-- true
false
xor: Bool → Bool → Bool
xor
false: Bool
false
false: Bool
false
-- false

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.

def 
nand: Bool → Bool → Bool
nand
:
Bool: Type
Bool
->
Bool: Type
Bool
->
Bool: Type
Bool
|
true: Bool
true
,
true: Bool
true
=>
Error: don't know how to synthesize placeholder context: Bool
|
true: Bool
true
,
false: Bool
false
=>
Error: don't know how to synthesize placeholder context: Bool
|
false: Bool
false
,
true: Bool
true
=>
Error: don't know how to synthesize placeholder context: Bool
|
false: Bool
false
,
false: Bool
false
=>
Error: don't know how to synthesize placeholder context: Bool

Complete this definition of the nor (not or) function. It must return the opposite of what the or function returns in each case.

def 
nor: Bool → Bool → Bool
nor
:
Bool: Type
Bool
->
Bool: Type
Bool
->
Bool: Type
Bool
:=
Error: don't know how to synthesize placeholder context: Bool Bool Bool
-- 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.

false
mystery: Bool → Bool → Bool
mystery
true: Bool
true
false: Bool
false
-- false -- b1 b2 (!b1 && !b2)

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.

def 
error_example: Bool → Bool
error_example
:
Bool: Type
Bool
Bool: Type
Bool
Error: missing cases: false
-- 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.

false
!
true: Bool
true
-- prefix notation for not
true
!
false: Bool
false
true
true: Bool
true
&&
true: Bool
true
-- infix notation for and
false
true: Bool
true
&&
false: Bool
false
false
false: Bool
false
&&
true: Bool
true
false
false: Bool
false
&&
false: Bool
false
true
true: Bool
true
||
true: Bool
true
-- infix notation for or
true
true: Bool
true
||
false: Bool
false
true
false: Bool
false
||
true: Bool
true
false
false: Bool
false
||
false: Bool
false
0
0: Nat
0
+
0: Nat
0
-- infix notation for Nat.add
"Hello, Logic!"
"Hello": String
"Hello"
++
", Logic!": String
", Logic!"
-- infix notation for String.append

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.

7
id_nat: Nat → Nat
id_nat
7: Nat
7
"Hello"
id_string: String → String
id_string
"Hello": String
"Hello"
true
id_bool: Bool → Bool
id_bool
true: Bool
true

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!
"Hello!"
(
id_poly: (α : Type) → α → α
id_poly
String: Type
String
)
"Hello!": String
"Hello!"
7
(
id_poly: (α : Type) → α → α
id_poly
Nat: Type
Nat
)
7: Nat
7
true
(
id_poly: (α : Type) → α → α
id_poly
Bool: Type
Bool
)
true: Bool
true

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 : (α : Type) α α
(
id_poly: (α : Type) → α → α
id_poly
) -- generalized definition
id_poly Nat : Nat Nat
(
id_poly: (α : Type) → α → α
id_poly
Nat: Type
Nat
) -- specialization to Nat
id_poly Bool : Bool Bool
(
id_poly: (α : Type) → α → α
id_poly
Bool: Type
Bool
) -- specialization to Bool
id_poly String : String String
(
id_poly: (α : Type) → α → α
id_poly
String: Type
String
) -- specialization to String

We can specialize the generalized function to specific types by applying it only to a first type argument.

def 
id_nat': Nat → Nat
id_nat'
:=
id_poly: (α : Type) → α → α
id_poly
Nat: Type
Nat
-- same as id_nat above def
id_string': String → String
id_string'
:=
id_poly: (α : Type) → α → α
id_poly
String: Type
String
-- same as id_string above def
id_bool': Bool → Bool
id_bool'
:=
id_poly: (α : Type) → α → α
id_poly
Bool: Type
Bool
-- same as id_bool above
7
id_nat': Nat → Nat
id_nat'
7: Nat
7
"Hello"
id_string': String → String
id_string'
"Hello": String
"Hello"
true
id_bool': Bool → Bool
id_bool'
true: Bool
true

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_poly Bool (sorryAx Bool true) : Bool
id_poly: (α : Type) → α → α
id_poly
Bool: Type
Bool
Error: failed to synthesize instance OfNat Bool 7
-- Lean can't convert 7 into a Bool
id_poly Bool true : Bool
id_poly: (α : Type) → α → α
id_poly
Bool: Type
Bool
true: Bool
true
id_poly Nat 7 : Nat
id_poly: (α : Type) → α → α
id_poly
Nat: Type
Nat
7: Nat
7
id_poly String "Hello" : String
id_poly: (α : Type) → α → α
id_poly
String: Type
String
"Hello": String
"Hello"

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.

7
id_poly'': {α : Type} → α → α
id_poly''
7: Nat
7
-- α = Nat, inferred!
"Hello!"
id_poly'': {α : Type} → α → α
id_poly''
"Hello!": String
"Hello!"
-- α = String, inferred!
true
id_poly'': {α : Type} → α → α
id_poly''
true: Bool
true
-- α = Bool, inferred! #eval
Error: function expected at id_poly'' ?m.31870 term has type ?m.31858
Error: function expected at id_poly'' ?m.31870 term has type ?m.31858
Error: application type mismatch id_poly'' Nat argument Nat has type Type : Type 1 but is expected to have type ?m.31858 : Type
Error: function expected at id_poly'' ?m.31870 term has type ?m.31858
-- error #eval
Error: function expected at id_poly'' ?m.31929 term has type ?m.31917
Error: function expected at id_poly'' ?m.31929 term has type ?m.31917
Error: application type mismatch id_poly'' String argument String has type Type : Type 1 but is expected to have type ?m.31917 : Type
Error: function expected at id_poly'' ?m.31929 term has type ?m.31917
-- error #eval
Error: function expected at id_poly'' ?m.31988 term has type ?m.31976
Error: function expected at id_poly'' ?m.31988 term has type ?m.31976
Error: application type mismatch id_poly'' Bool argument Bool has type Type : Type 1 but is expected to have type ?m.31976 : Type
Error: function expected at id_poly'' ?m.31988 term has type ?m.31976
-- 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.

7
@
id_poly'': {α : Type} → α → α
id_poly''
Nat: Type
Nat
7: Nat
7
-- α = Nat, inferred!
"Hello!"
@
id_poly'': {α : Type} → α → α
id_poly''
String: Type
String
"Hello!": String
"Hello!"
-- α = String, inferred!
true
@
id_poly'': {α : Type} → α → α
id_poly''
Bool: Type
Bool
true: Bool
true
-- α = Bool, inferred!

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 Nat
(
Nat.succ: Nat → Nat
Nat.succ
) -- Nat → Nat
1
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
-- 1
2
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
-- expect 2
5
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
Nat.succ: Nat → Nat
Nat.succ
3: Nat
3
-- expect 5

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.

def 
double: Nat → Nat
double
:
Nat: Type
Nat
Nat: Type
Nat
|
n: Nat
n
=>
2: Nat
2
*
n: Nat
n
16
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
double: Nat → Nat
double
4: Nat
4
-- expect 16 (2 * (2 * 4))
40
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
double: Nat → Nat
double
10: Nat
10
-- expect 40 (2 * (2 * 10))

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:

def 
square: Nat → Nat
square
:
Nat: Type
Nat
Nat: Type
Nat
|
n: Nat
n
=>
n: Nat
n
^
2: Nat
2
16
square: Nat → Nat
square
4: Nat
4
-- expect 16

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.

1
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
square: Nat → Nat
square
1: Nat
1
-- expect 1
16
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
square: Nat → Nat
square
2: Nat
2
-- expect 16
81
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
square: Nat → Nat
square
3: Nat
3
-- expect 81
256
apply2_nat: (Nat → Nat) → Nat → Nat
apply2_nat
square: Nat → Nat
square
4: Nat
4
-- expect 256

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 ++ "!".

def 
exclaim: String → String
exclaim
:
String: Type
String
String: Type
String
|
s: String
s
=>
s: String
s
++
"!": String
"!"
"Hello!"
exclaim: String → String
exclaim
"Hello": String
"Hello"
-- apply it once
"Hello!!"
exclaim: String → String
exclaim
(
exclaim: String → String
exclaim
"Hello": String
"Hello"
) -- apply it twice

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.

def 
apply2_string: (String → String) → String → String
apply2_string
: (
String: Type
String
String: Type
String
)
String: Type
String
String: Type
String
|
f: String → String
f
,
a: String
a
=>
f: String → String
f
(
f: String → String
f
a: String
a
)
"Hello!!"
apply2_string: (String → String) → String → String
apply2_string
exclaim: String → String
exclaim
"Hello": String
"Hello"
-- expect "Hello!!"

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.

def 
apply2': (α : Type) → (α → α) → α → α
apply2'
: (
α: Type
α
:
Type: Type 1
Type
) (
α: Type
α
α: Type
α
)
α: Type
α
α: Type
α
| _,
f: x✝ → x✝
f
,
a: x✝
a
=>
f: x✝ → x✝
f
(
f: x✝ → x✝
f
a: 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! -/
2
apply2': (α : Type) → (α → α) → α → α
apply2'
Nat: Type
Nat
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
-- expect 2
4
apply2': (α : Type) → (α → α) → α → α
apply2'
Nat: Type
Nat
double: Nat → Nat
double
1: Nat
1
-- expect 4
16
apply2': (α : Type) → (α → α) → α → α
apply2'
Nat: Type
Nat
square: Nat → Nat
square
2: Nat
2
-- expect 16
"Hello!!"
apply2': (α : Type) → (α → α) → α → α
apply2'
String: Type
String
exclaim: String → String
exclaim
"Hello": String
"Hello"
-- "Hello!!"

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:

def 
apply2: {α : Type} → (α → α) → α → α
apply2
: {
α: Type
α
:
Type: Type 1
Type
} (
α: Type
α
α: Type
α
)
α: Type
α
α: Type
α
| _,
f: x✝ → x✝
f
,
a: x✝
a
=>
f: x✝ → x✝
f
(
f: x✝ → x✝
f
a: x✝
a
) -- Now the type arguments are implicit!
2
apply2: {α : Type} → (α → α) → α → α
apply2
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
-- expect 2
4
apply2: {α : Type} → (α → α) → α → α
apply2
double: Nat → Nat
double
1: Nat
1
-- expect 4
16
apply2: {α : Type} → (α → α) → α → α
apply2
square: Nat → Nat
square
2: Nat
2
-- expect 16
"Hello!!"
apply2: {α : Type} → (α → α) → α → α
apply2
exclaim: String → String
exclaim
"Hello": String
"Hello"
-- Hello!!

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_04

Function 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.

20
apply2: {α : Type} → (α → α) → α → α
apply2
double: Nat → Nat
double
5: Nat
5
-- (double (double 5) -- (double ∘ double) 5 -- expect 20

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.

20
apply2: {α : Type} → (α → α) → α → α
apply2
double: Nat → Nat
double
5: Nat
5
-- expect 20
20
(
apply2: {α : Type} → (α → α) → α → α
apply2
double: Nat → Nat
double
)
5: Nat
5
-- exactly the same

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.

def 
double_after_double: Nat → Nat
double_after_double
:= (
apply2: {α : Type} → (α → α) → α → α
apply2
double: Nat → Nat
double
) -- Sure enough it's a function from Nat to Nat
double_after_double : Nat Nat
(
double_after_double: Nat → Nat
double_after_double
) -- And it *behaves* just as expected
0
double_after_double: Nat → Nat
double_after_double
0: Nat
0
-- expect 0 (double (double 0))
4
double_after_double: Nat → Nat
double_after_double
1: Nat
1
-- expect 4 (double (double 1))
20
double_after_double: Nat → Nat
double_after_double
5: Nat
5
-- expect 20 (double (double 5))

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.

def 
square_after_square: Nat → Nat
square_after_square
:=
apply2: {α : Type} → (α → α) → α → α
apply2
square: Nat → Nat
square
-- square ∘ square
625
square_after_square: Nat → Nat
square_after_square
5: Nat
5
-- expect 625 def
exclaim_after_exclaim: String → String
exclaim_after_exclaim
:=
apply2: {α : Type} → (α → α) → α → α
apply2
exclaim: String → String
exclaim
-- exclaim ∘ exclaim
"Love math!!"
exclaim_after_exclaim: String → String
exclaim_after_exclaim
"Love math": String
"Love math"
-- "Love math!!"

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
def 
len: String → Nat
len
:
String: Type
String
Nat: Type
Nat
:=
String.length: String → Nat
String.length
len : String Nat
(
len: String → Nat
len
) -- String → Nat -- We need a function of type Nat → Bool def
ev: Nat → Bool
ev
(
n: Nat
n
:
Nat: Type
Nat
) :
Bool: Type
Bool
:=
n: Nat
n
%
2: Nat
2
=
0: Nat
0
ev : Nat Bool
(
ev: Nat → Bool
ev
) -- Nat → Bool -- glue_funs composes ev and len into a String → Bool function!
false
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
ev: Nat → Bool
ev
len: String → Nat
len
"Hello": String
"Hello"
-- expect false -- Remember application is left associative
false
(
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
ev: Nat → Bool
ev
len: String → Nat
len
)
"Hello": String
"Hello"
-- expect false -- (glue_funs ev len) is the function we want!
glue_funs ev len : String Bool
(
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
ev: Nat → Bool
ev
len: String → Nat
len
) -- String → Bool -- Applied to a String it gives back a Bool!
true
(
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
ev: Nat → Bool
ev
len: String → Nat
len
)
"Hello!": String
"Hello!"
-- expect true -- We can even name this function then use it. def
ev_string: String → Bool
ev_string
:= (
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
ev: Nat → Bool
ev
len: String → Nat
len
)
false
ev_string: String → Bool
ev_string
"Hi!": String
"Hi!"
-- expect false
false
ev_string: String → Bool
ev_string
"Hello": String
"Hello"
-- expect false
true
ev_string: String → Bool
ev_string
"": String
""
-- expect true
true
ev_string: String → Bool
ev_string
"I Love Logic": String
"I Love Logic"
-- true

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.

64
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
square: Nat → Nat
square
double: Nat → Nat
double
4: Nat
4
-- expect 64 -- Apply glue_funs to first two arguments def
square_after_double: Nat → Nat
square_after_double
:=
glue_funs: {α β γ : Type} → (β → γ) → (α → β) → α → γ
glue_funs
square: Nat → Nat
square
double: Nat → Nat
double
-- Apply the resulting function to 4
64
square_after_double: Nat → Nat
square_after_double
4: Nat
4
-- (square ∘ double) 4 -- square (double 4) -- square 8 -- 64

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.

def 
is_even_len: ?m.46075
is_even_len
Error: failed to infer definition type
Error: failed to infer definition type
Error: don't know how to synthesize placeholder context: ?m.46075
-- Be sure you can solve it #eval
Error: unknown identifier 'is_even_len'
"Love": ?m.46079
"Love"
-- Expect true #eval
Error: unknown identifier 'is_even_len'
"Love!": ?m.46098
"Love!"
-- Expect false #eval
Error: unknown identifier 'is_even_len'
"Love!!": ?m.46117
"Love!!"
-- Expect true

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
def 
fun_double: Nat → Nat
fun_double
:= fun (
n: Nat
n
:
Nat: Type
Nat
) =>
2: Nat
2
*
n: Nat
n
def
fun_square: Nat → Nat
fun_square
:= fun (
n: Nat
n
:
Nat: Type
Nat
) =>
n: Nat
n
^
2: Nat
2
-- Here we pass an unnamed function to composee
false
compose: {α β γ : Type} → (β → γ) → (α → β) → α → γ
compose
(fun (
n: Nat
n
:
Nat: Type
Nat
) =>
n: Nat
n
%
2: Nat
2
==
0: Nat
0
)
String.length: String → Nat
String.length
"Love!": String
"Love!"

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
def 
double_after_square: Nat → Nat
double_after_square
:= (
double: Nat → Nat
double
square: Nat → Nat
square
) def
is_even_len'': String → Bool
is_even_len''
:= (
is_even: Nat → Bool
is_even
String.length: String → Nat
String.length
)
50
double_after_square: Nat → Nat
double_after_square
5: Nat
5
-- expect 50
25
String.length: String → Nat
String.length
"Hello Higher Mathematics!": String
"Hello Higher Mathematics!"
-- 25
false
is_even_len'': String → Bool
is_even_len''
"Hello Higher Mathematics!": String
"Hello Higher Mathematics!"
-- false

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 Object

RPS.Object.rock : Object
rock: Object
rock
-- type is object
RPS.Object.paper : Object
paper: Object
paper
-- type is object
RPS.Object.scissors : Object
scissors: Object
scissors
-- type is object
RPS.Object : Type
Object: Type
Object
-- By golly it's a 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.

def 
play: Object → Object → Result
play
:
Object: Type
Object
Object: Type
Object
Result: Type
Result
|
scissors: Object
scissors
,
scissors: Object
scissors
=>
ties: Result
ties
|
scissors: Object
scissors
,
paper: Object
paper
=>
wins: Result
wins
|
scissors: Object
scissors
,
rock: Object
rock
=>
loses: Result
loses
|
paper: Object
paper
,
scissors: Object
scissors
=>
loses: Result
loses
|
paper: Object
paper
,
paper: Object
paper
=>
ties: Result
ties
|
paper: Object
paper
,
rock: Object
rock
=>
wins: Result
wins
|
rock: Object
rock
,
scissors: Object
scissors
=>
wins: Result
wins
|
rock: Object
rock
,
paper: Object
paper
=>
loses: Result
loses
|
rock: Object
rock
,
rock: Object
rock
=>
ties: Result
ties
loses
play: Object → Object → Result
play
scissors: Object
scissors
rock: Object
rock
end RPS

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 type
Box Nat : Type
Box: Type → Type
Box
Nat: Type
Nat
-- A put application term is a value of this type
put 1 : Box Nat
(
put: {α : Type} → α → Box α
put
1: Nat
1
) -- this term is a value of type Box Nat
put 1
(
put: {α : Type} → α → Box α
put
1: Nat
1
) -- constructors don't compute/do anything -- We can assign values of our type to to variables def
box_containing_zero: Box Nat
box_containing_zero
:
Box: Type → Type
Box
Nat: Type
Nat
:=
put: {α : Type} → α → Box α
put
0: Nat
0
-- Box is a type builder, taking arguments of different types def
box_containing_hello: Box String
box_containing_hello
:
Box: Type → Type
Box
String: Type
String
:=
put: {α : Type} → α → Box α
put
"Hello": String
"Hello"
-- Lean can usually infer the type of a constructor term def
box_containing_hello': Box String
box_containing_hello'
:=
put: {α : Type} → α → Box α
put
"Hello": String
"Hello"
-- What is the type of Box? It takes a type and yields a type
Box : Type Type
(
Box: Type → Type
Box
) -- Type → Type (study and understand this) -- A constructor of a polyorphic type is itself polymorphic
@put : {α : Type} α Box α
(@
Box.put: {α : Type} → α → Box α
Box.put
) -- {α : Type} → α → Box α. Understand it!

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 : {α : Type} α Box α
(@
put: {α : Type} → α → Box α
put
) -- @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.

1
get: {α : Type} → Box α → α
get
(
put: {α : Type} → α → Box α
put
1: Nat
1
) -- o matches with 11
true
get: {α : Type} → Box α → α
get
(
put: {α : Type} → α → Box α
put
true: Bool
true
) -- o matches with true
"Hello!"
get: {α : Type} → Box α → α
get
(
put: {α : Type} → α → Box α
put
"Hello!": String
"Hello!"
) -- o matches with "Hello!"

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 : {α β : Type} α β Prod α β
(@
pair: {α β : Type} → α → β → Prod α β
pair
) -- Here we build two ordered pair values def
a_pair_string_nat: Prod String Nat
a_pair_string_nat
:
Prod: Type → Type → Type
Prod
String: Type
String
Nat: Type
Nat
:=
pair: {α β : Type} → α → β → Prod α β
pair
"Love": String
"Love"
4: Nat
4
def
a_pair_nat_bool: Prod Nat Bool
a_pair_nat_bool
:
Prod: Type → Type → Type
Prod
Nat: Type
Nat
Bool: Type
Bool
:=
pair: {α β : Type} → α → β → Prod α β
pair
5: Nat
5
false: Bool
false
-- These objects are of the "parameterized" types you expect
cs2120.a_pair_string_nat : Prod String Nat
a_pair_string_nat: Prod String Nat
a_pair_string_nat
-- type of ("Love",5) is (Prod String Nat)
cs2120.a_pair_nat_bool : Prod Nat Bool
a_pair_nat_bool: Prod Nat Bool
a_pair_nat_bool
-- type of (5, false) is (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.

"Love"
first: {α β : Type} → Prod α β → α
first
a_pair_string_nat: Prod String Nat
a_pair_string_nat
-- "Love"
4
second: {α β : Type} → Prod α β → β
second
a_pair_string_nat: Prod String Nat
a_pair_string_nat
-- 4
5
first: {α β : Type} → Prod α β → α
first
a_pair_nat_bool: Prod Nat Bool
a_pair_nat_bool
-- 5
false
second: {α β : Type} → Prod α β → β
second
a_pair_nat_bool: Prod Nat Bool
a_pair_nat_bool
-- false
2
first: {α β : Type} → Prod α β → α
first
(
pair: {α β : Type} → α → β → Prod α β
pair
2: Nat
2
"Hi": String
"Hi"
) -- 2
"Hi"
second: {α β : Type} → Prod α β → β
second
(
pair: {α β : Type} → α → β → Prod α β
pair
2: Nat
2
"Hi": String
"Hi"
) -- "Hi"

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
def 
pair0: Bool × Nat
pair0
:=
Prod.mk: {α β : Type} → α → β → α × β
Prod.mk
true: Bool
true
10: Nat
10
pair0 : Bool × Nat
pair0: Bool × Nat
pair0
-- Bool × Nat -- Use standard notation instead of Prod.mk def
pair1: String × Nat
pair1
:= (
"Hello": String
"Hello"
,
5: Nat
5
) def
pair2: Nat × Bool
pair2
:= (
17: Nat
17
,
false: Bool
false
)
pair1 : String × Nat
pair1: String × Nat
pair1
-- Type is String × Nat
pair2 : Nat × Bool
pair2: Nat × Bool
pair2
-- Type is Nat × Bool -- Be sure you understand these function types -- The u_1 and u_2 generalize to Type, Type 1, ...
@Prod.fst : {α : Type u_1} {β : Type u_2} α × β α
(@
Prod.fst: {α : Type u_1} → {β : Type u_2} → α × β → α
Prod.fst
)
@Prod.snd : {α : Type u_1} {β : Type u_2} α × β β
(@
Prod.snd: {α : Type u_1} → {β : Type u_2} → α × β → β
Prod.snd
) -- How to eliminate to get the first or second element
"Hello"
Prod.fst: {α β : Type} → α × β → α
Prod.fst
pair1: String × Nat
pair1
-- expect "Hello"
5
Prod.snd: {α β : Type} → α × β → β
Prod.snd
pair1: String × Nat
pair1
-- expect 5
17
Prod.fst: {α β : Type} → α × β → α
Prod.fst
pair2: Nat × Bool
pair2
-- expect 17
false
Prod.snd: {α β : Type} → α × β → β
Prod.snd
pair2: Nat × Bool
pair2
-- expect false -- We call these "projection" functions -- Lean provides notations for "projection"
"Hello"
pair1: String × Nat
pair1
.
1: {α β : Type} → α × β → α
1
-- expect "Hello"
5
pair1: String × Nat
pair1
.
2: {α β : Type} → α × β → β
2
-- expect 5
17
pair2: Nat × Bool
pair2
.
1: {α β : Type} → α × β → α
1
-- expect 17
false
pair2: Nat × Bool
pair2
.
2: {α β : Type} → α × β → β
2
-- expect false

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

inductive 
Box: Type → Type
Box
(
α: Type
α
:
Type: Type 1
Type
) :
Type: Type 1
Type
|
put: {α : Type} → α → Box α
put
(
a: α
a
:
α: Type
α
)
@Box.put : {α : Type} α Box α
(@
Box.put: {α : Type} → α → Box α
Box.put
) def
foo: Box String
foo
:= (
Box.put: {α : Type} → α → Box α
Box.put
"Hello": String
"Hello"
)
cs2120.foo : Box String
foo: Box String
foo

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 : {α : Type} α Box α
(@
Box.put: {α : Type} → α → Box α
Box.put
) def
jack_in_a_box: Box String
jack_in_a_box
:= @
Box.put: {α : Type} → α → Box α
Box.put
String: Type
String
"Jack!": String
"Jack!"

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.

def 
get: {α : Type} → Box α → α
get
{
α: Type
α
:
Type: Type 1
Type
}:
Box: Type → Type
Box
α: Type
α
α: Type
α
| (
Box.put: {α : Type} → α → Box α
Box.put
s: α
s
) =>
s: α
s
"Jack!"
get: {α : Type} → Box α → α
get
(
Box.put: {α : Type} → α → Box α
Box.put
"Jack!": String
"Jack!"
) def
square: Nat → Nat
square
(
n: Nat
n
:
Nat: Type
Nat
) :
Nat: Type
Nat
:=
n: Nat
n
*
n: Nat
n

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

Nat × Bool : Type
(
Prod: Type → Type → Type
Prod
Nat: Type
Nat
Bool: Type
Bool
) -- a type
(3, true) : Nat × Bool
(
Prod.mk: {α β : Type} → α → β → α × β
Prod.mk
3: Nat
3
true: Bool
true
) -- a value (term)
(3, true) : Nat × Bool
(
3: Nat
3
,
true: Bool
true
) -- outfix notation -- aka *projection functions*
3
Prod.fst: {α β : Type} → α × β → α
Prod.fst
(
3: Nat
3
,
true: Bool
true
)
true
Prod.snd: {α β : Type} → α × β → β
Prod.snd
(
3: Nat
3
,
true: Bool
true
)
3
(
3: Nat
3
,
true: Bool
true
).
1: {α β : Type} → α × β → α
1
-- postfix notation
true
(
3: Nat
3
,
true: Bool
true
).
2: {α β : Type} → α × β → β
2
-- postfix notation

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.

def 
nat_to_string: Nat → String
nat_to_string
(
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
) :=
"It's a Nat": String
"It's a Nat"
-- argument unused def
bool_to_string: Bool → String
bool_to_string
(
Warning: unused variable `b` [linter.unusedVariables]
:
Bool: Type
Bool
) :=
"It's a Bool": String
"It's a Bool"

Now we can apply the elimination function we defined.

"It's a Nat"
elim_sum2: Sum Nat Bool → (Nat → String) → (Bool → String) → String
elim_sum2
a_sum1: Sum Nat Bool
a_sum1
nat_to_string: Nat → String
nat_to_string
bool_to_string: Bool → String
bool_to_string
"It's a Bool"
elim_sum2: Sum Nat Bool → (Nat → String) → (Bool → String) → String
elim_sum2
b_sum1: Sum Nat Bool
b_sum1
nat_to_string: Nat → String
nat_to_string
bool_to_string: Bool → String
bool_to_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.

def 
elim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γ
elim_sum
{
α: Type
α
β: Type
β
γ: Type
γ
:
Type: Type 1
Type
} : (
Sum: Type → Type → Type
Sum
α: Type
α
β: Type
β
) (
α: Type
α
γ: Type
γ
) (
β: Type
β
γ: Type
γ
)
γ: Type
γ
| (
Sum.inl: {α β : Type} → α → Sum α β
Sum.inl
a: α
a
),
α2γ: α → γ
α2γ
, _ =>
α2γ: α → γ
α2γ
a: α
a
| (
Sum.inr: {α β : Type} → β → Sum α β
Sum.inr
b: β
b
), _,
β2γ: β → γ
β2γ
=>
β2γ: β → γ
β2γ
b: β
b
"It's a Nat"
elim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γ
elim_sum
a_sum1: Sum Nat Bool
a_sum1
nat_to_string: Nat → String
nat_to_string
bool_to_string: Bool → String
bool_to_string
"It's a Bool"
elim_sum: {α β γ : Type} → Sum α β → (α → γ) → (β → γ) → γ
elim_sum
b_sum1: Sum Nat Bool
b_sum1
nat_to_string: Nat → String
nat_to_string
bool_to_string: Bool → String
bool_to_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.

def 
s: Nat ⊕ ?m.26264
s
Error: failed to infer definition type
Error: failed to infer definition type
Error: don't know how to synthesize implicit argument @Sum.inl Nat ?m.26264 1 context: Type u_1
-- don't know how to synthesize implicit argument def s1 : Sum Nat Bool := Sum.inl 1 def s2 : Sum Nat Bool := Sum.inr true
s1 : Nat Bool
s1: Nat ⊕ Bool
s1
s2 : Nat Bool
s2: Nat ⊕ Bool
s2
def
which: Nat ⊕ Bool → String
which
:
Sum: Type → Type → Type
Sum
Nat: Type
Nat
Bool: Type
Bool
String: Type
String
| (
Sum.inl: {α : Type ?u.26339} → {β : Type ?u.26338} → α → α ⊕ β
Sum.inl
_) =>
"Left": String
"Left"
| (
Sum.inr: {α : Type ?u.26360} → {β : Type ?u.26359} → β → α ⊕ β
Sum.inr
_) =>
"Right": String
"Right"
"Left"
which: Nat ⊕ Bool → String
which
s1: Nat ⊕ Bool
s1
"Right"
which: Nat ⊕ Bool → String
which
s2: Nat ⊕ Bool
s2

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 "()" => 
unit: Unit
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. Here's one. It takes a Nat value and returns a Unit value.

def 
useless: Nat → Unit
useless
(
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
) :
Unit: Type
Unit
:=
(): Unit
()
()
useless: Nat → Unit
useless
0: Nat
0

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

def 
main: IO Unit
main
:
IO: Type → Type
IO
Unit: Type
Unit
:=
IO.println: {α : Type} → [inst : ToString α] → α → IO Unit
IO.println
"Hello, World!": String
"Hello, World!"
-- Lean can run this code for us using #eval
Hello, World!
main: IO Unit
main

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.unit : Unit
Unit.unit: Unit
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
def 
nat2empty: Nat → Empty
nat2empty
:
Nat: Type
Nat
Empty: Type
Empty
|
n: Nat
n
=>
Error: don't know how to synthesize placeholder context: x : Nat n : Nat := x Empty

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.)

sorryAx (Nat Empty) true 5
nat2empty: Nat → Empty
nat2empty
5: Nat
5
-- sorry (doesn't properly reduce)

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!
def 
x: Nat
x
:= (
empty2nat: Empty → Nat
empty2nat
Error: don't know how to synthesize placeholder context: Empty
) -- 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.

def 
empty2empty: Empty → Empty
empty2empty
:
Empty: Type
Empty
Empty: Type
Empty
|
e: Empty
e
=> nomatch
e: Empty
e
def
x': Empty
x'
:= (
empty2empty: Empty → Empty
empty2empty
Error: don't know how to synthesize placeholder context: 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*
def 
function_type: Type → Type → Type
function_type
(
α: Type
α
β: Type
β
:
Type: Type 1
Type
) :
Type: Type 1
Type
:=
α: Type
α
β: Type
β
Nat Bool
(
function_type: Type → Type → Type
function_type
Nat: Type
Nat
Bool: Type
Bool
)
function_type Nat Bool : Type
(
function_type: Type → Type → Type
function_type
Nat: Type
Nat
Bool: Type
Bool
)

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)
def 
negate': Bool → Bool
negate'
:
Bool: Type
Bool
Bool: Type
Bool
:= -- type fun
x: Bool
x
:
Bool: Type
Bool
=> -- assume given Bool x match
x: Bool
x
with -- case analysis on x |
true: Bool
true
=>
false: Bool
false
-- result in case true |
false: Bool
false
=>
true: Bool
true
-- result in case false -- A *fun* term expresses a function *implementation*
fun x => match x with | true => false | false => true : Bool Bool
(fun
x: Bool
x
:
Bool: Type
Bool
=> match
x: Bool
x
with |
true: Bool
true
=>
false: Bool
false
|
false: Bool
false
=>
true: Bool
true
) -- Sometimes *fun* is written as Greek lambda *λ*
fun x => match x with | true => false | false => true : Bool Bool
(λ
x: Bool
x
:
Bool: Type
Bool
=> match
x: Bool
x
with |
true: Bool
true
=>
false: Bool
false
|
false: Bool
false
=>
true: Bool
true
)

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} α β α × β : Type 1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} (
a: α
a
:
α: Type
α
) (
b: β
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", 5) : String × Nat
(
"Hello": String
"Hello"
,
5: Nat
5
) -- value of type String × 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} α × β α : Type 1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
}
α: Type
α
×
β: Type
β
α: Type
α
-- α × β is Prod α β
{α β : Type} α × β β : Type 1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
}
α: Type
α
×
β: Type
β
β: Type
β
-- Sum construction and elimination
{α β : Type} α α β : Type 1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
}
α: Type
α
α: Type
α
β: Type
β
-- α ⊕ β is Sum α β
{α β : Type} β α β : Type 1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
}
β: Type
β
α: Type
α
β: Type
β
{α β γ : Type} α β γ) γ) γ : Type 1
{
α: Type
α
β: Type
β
γ: Type
γ
:
Type: Type 1
Type
}
α: Type
α
β: Type
β
(
α: Type
α
γ: Type
γ
) (
β: Type
β
γ: Type
γ
)
γ: Type
γ
-- Unit construction
Unit.unit : Unit
Unit.unit: Unit
Unit.unit
-- There is no useful way to use a value of this type -- There is no constructor for Empty -- Empty elimination
{α : Type} Empty α : Type 1
{
α: Type
α
:
Type: Type 1
Type
}
Empty: Type
Empty
α: Type
α
-- Function composition
{α β γ : Type} γ) β) α γ : Type 1
{
α: Type
α
β: Type
β
γ: Type
γ
:
Type: Type 1
Type
} (
β: 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.unit: Unit
Unit.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).

def 
useless: Nat → Unit
useless
:
Nat: Type
Nat
Unit: Type
Unit
:= fun
_: Nat
_
=>
(): Unit
()
()
useless: Nat → Unit
useless
0: Nat
0
-- returns (), void

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

def 
main: IO Unit
main
:
IO: Type → Type
IO
Unit: Type
Unit
:=
IO.println: {α : Type} → [inst : ToString α] → α → IO Unit
IO.println
"Hello, World!": String
"Hello, World!"
-- Lean can run this code for us using #eval
Hello, World!
main: IO Unit
main

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.unit : Unit
Unit.unit: Unit
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.

def 
e: Empty
e
:
Empty: Type
Empty
:=
Error: don't know how to synthesize placeholder context: Empty
-- 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 => 
Error: don't know how to synthesize placeholder context: x : Unit unit : Unit := x Empty
-- 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' 
Error: don't know how to synthesize placeholder for argument 'e' context: 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 cs2120

Data 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'

def 
inner: Doll → Doll
inner
:
Doll: Type
Doll
Doll: Type
Doll
|
solid: Doll
solid
=>
solid: Doll
solid
| (
shell: Doll → Doll
shell
d': Doll
d'
) =>
d': Doll
d'
shell (shell solid)
inner: Doll → Doll
inner
d3: Doll
d3
-- expect (shell (shell solid))
solid
inner: Doll → Doll
inner
solid: Doll
solid

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.

0
depth: Doll → Nat
depth
d0: Doll
d0
1
depth: Doll → Nat
depth
d1: Doll
d1
2
depth: Doll → Nat
depth
d2: Doll
d2
3
depth: Doll → Nat
depth
d3: Doll
d3

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 
Error: fail to show termination for bad_depth' with errors structural recursion cannot be used well-founded recursion cannot be used, 'bad_depth'' does not take any (non-fixed) arguments
:
Doll: Type
Doll
Nat: Type
Nat
|
d: Doll
d
=>
bad_depth': Doll → Nat
bad_depth'
d: Doll
d
-- not a structural recursion

There's a deep reason Lean can't accept such a definition. Suppose it could. Then we could write the following slight variant.

def 
Error: fail to show termination for bad_depth with errors structural recursion cannot be used well-founded recursion cannot be used, 'bad_depth' does not take any (non-fixed) arguments
:
Doll: Type
Doll
Empty: Type
Empty
-- Lean rejects def |
d: Doll
d
=>
bad_depth: Doll → Empty
bad_depth
d: Doll
d
bad_depth d3 : Empty
(
bad_depth: Doll → Empty
bad_depth
d3: Doll
d3
) -- Term of type Empty!!!
bad_depth (shell (shell (shell solid)))
(
bad_depth: Doll → Empty
bad_depth
d3: Doll
d3
) -- Function won't reduce

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.

def 
n0: Nat
n0
:=
Nat.zero: Nat
Nat.zero
def
n1: Nat
n1
:=
Nat.succ: Nat → Nat
Nat.succ
n0: Nat
n0
def
n2: Nat
n2
:=
Nat.succ: Nat → Nat
Nat.succ
n1: Nat
n1
def
n3: Nat
n3
:=
Nat.succ: Nat → Nat
Nat.succ
n2: Nat
n2
def
is_zero'': Nat → Bool
is_zero''
:
Nat: Type
Nat
Bool: Type
Bool
-- this works but is verbose |
Nat.zero: Nat
Nat.zero
=>
true: Bool
true
| (
Nat.succ: Nat → Nat
Nat.succ
Warning: unused variable `n'` [linter.unusedVariables]
) =>
false: Bool
false
def
is_zero': Nat → Bool
is_zero'
:
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
=>
true: Bool
true
Error: invalid patterns, `n'` is an explicit pattern variable, but it only occurs in positions that are inaccessible to pattern matching .(Nat.add 1 n')
-- (1 + n') not a valid pattern def is_Zero' : Nat Bool -- our preferred notation | 0 => true -- 0 for Nat.zero |
Warning: unused variable `n'` [linter.unusedVariables]
+ 1 =>
false: Bool
false
-- n' + 1 is a valid pattern

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.

def 
pred: Nat → Nat
pred
:
Nat: Type
Nat
Nat: Type
Nat
|
0: Nat
0
=>
0: Nat
0
| (
Nat.succ: Nat → Nat
Nat.succ
n': Nat
n'
) =>
n': Nat
n'
-- destructures nat>0 to (succ n') -- Answer here -- test cases
2
pred: Nat → Nat
pred
3: Nat
3
-- expect 2
0
pred: Nat → Nat
pred
0: Nat
0
-- expect 0

#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

def 
mk_doll: Nat → Doll
mk_doll
:
Nat: Type
Nat
Doll: Type
Doll
| _ =>
_: Doll
_
Error: redundant alternative
-- test cases
mk_doll 3 : Doll
mk_doll: Nat → Doll
mk_doll
3: Nat
3
sorryAx (Nat Doll) true 3
mk_doll: Nat → Doll
mk_doll
3: Nat
3

#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?

def 
nat_eq: Nat → Nat → Bool
nat_eq
:
Nat: Type
Nat
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
,
0: Nat
0
=>
true: Bool
true
|
0: Nat
0
,
n': Nat
n'
+ 1 =>
false: Bool
false
|
n': Nat
n'
+ 1,
0: Nat
0
=>
false: Bool
false
| (
n': Nat
n'
+ 1), (
m': Nat
m'
+ 1) =>
nat_eq: Nat → Nat → Bool
nat_eq
Error: don't know how to synthesize placeholder context: n' m' : Nat Nat
Error: don't know how to synthesize placeholder context: n' m' : Nat Nat
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors

#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
| (_), (_) => 
Error: don't know how to synthesize placeholder context: x✝¹ x : Nat Bool
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- exect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect false
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect false
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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') => 
Error: don't know how to synthesize placeholder context: m n' : Nat Nat

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}
truefalse
falsetrue

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}
truetrue
falsetrue

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.

ab{a} ∨ {b}
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

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:

ab{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.

2
var.n: var → Nat
var.n
v₂: var
v₂
-- application notation open var -- not a good idea
2
n: var → Nat
n
v₂: var
v₂
-- but it works
2
v₂: var
v₂
.
n: var → Nat
n
-- dot notation

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

true
eval_expr: Expr → Interp → Bool
eval_expr
e0: Expr
e0
all_true: Interp
all_true
false
eval_expr: Expr → Interp → Bool
eval_expr
e1: Expr
e1
all_true: Interp
all_true
false
eval_expr: Expr → Interp → Bool
eval_expr
e2: Expr
e2
all_true: Interp
all_true
true
eval_expr: Expr → Interp → Bool
eval_expr
e3: Expr
e3
all_true: Interp
all_true
true
eval_expr: Expr → Interp → Bool
eval_expr
e4: Expr
e4
all_true: Interp
all_true
false
eval_expr: Expr → Interp → Bool
eval_expr
e0: Expr
e0
all_false: Interp
all_false
true
eval_expr: Expr → Interp → Bool
eval_expr
e1: Expr
e1
all_false: Interp
all_false
false
eval_expr: Expr → Interp → Bool
eval_expr
e2: Expr
e2
all_false: Interp
all_false
true
eval_expr: Expr → Interp → Bool
eval_expr
e3: Expr
e3
all_false: Interp
all_false
false
eval_expr: Expr → Interp → Bool
eval_expr
e4: Expr
e4
all_false: Interp
all_false

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.

variableintended meaning
rred block is on blue block
yyellow 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 α*.

def 
b: var
b
:=
var.mk: Nat → var
var.mk
0: Nat
0
def
j: var
j
:=
var.mk: Nat → var
var.mk
1: Nat
1
def
c: var
c
:=
var.mk: Nat → var
var.mk
2: Nat
2
def
a: var
a
:=
var.mk: Nat → var
var.mk
3: Nat
3
-- get the index out of the c structure
2
c: var
c
.
n: var → Nat
n

#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.

true
eval_expr: Expr → Interp → Bool
eval_expr
e0: Expr
e0
(λ
_: var
_
=>
false: Bool
false
) -- expect true
true
eval_expr: Expr → Interp → Bool
eval_expr
e0: Expr
e0
(λ
_: var
_
=>
true: Bool
true
) -- expect true -- You do the rest

#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 here

Propositional 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₀}
TT
FF

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₀FF
i₁FT
i₂TF
i₃TT

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₃
0FFF
1FFT
2FTF
3FTT
4TFF
5TFT
6TTF
7TTT

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₃
0000
1001
2010
3011
4100
5101
6110
7111

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.

def 
right_bit: Nat → Nat
right_bit
(
n: Nat
n
:
Nat: Type
Nat
) :=
n: Nat
n
%
2: Nat
2
def
shift_right: Nat → Nat
shift_right
(
n: Nat
n
:
Nat: Type
Nat
) :=
n: Nat
n
/
2: Nat
2
def
Warning: declaration uses 'sorry'
Warning: declaration uses 'sorry'
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
1: Nat
1
=> [
1: Nat
1
] |
n': Nat
n'
+ 2 => have : (
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) < (
n': Nat
n'
+
2: Nat
2
) :=
sorry: shift_right (n' + 2) < n' + 2
sorry
nat_to_bin: Nat → List Nat
nat_to_bin
(
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) ++ [
right_bit: Nat → Nat
right_bit
(
n': Nat
n'
+
2: Nat
2
)]

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.

[0]
nat_to_bin: Nat → List Nat
nat_to_bin
0: Nat
0
-- expect [0]
[1]
nat_to_bin: Nat → List Nat
nat_to_bin
1: Nat
1
-- expect [1]
[1, 1]
nat_to_bin: Nat → List Nat
nat_to_bin
3: Nat
3
-- expect [1,1]
[1, 0, 1]
nat_to_bin: Nat → List Nat
nat_to_bin
5: Nat
5
-- expect [1,0,1]
[1, 1, 0]
nat_to_bin: Nat → List Nat
nat_to_bin
6: Nat
6
-- expect [1,1,0]

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.

def 
zero_pad: Nat → List Nat → List Nat
zero_pad
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
v: Nat
v
,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
(
v: Nat
v
- (
l: List Nat
l
.
length: {α : Type} → List α → Nat
length
))
l: List Nat
l
where
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
,
l: List Nat
l
=>
l: List Nat
l
|
v': Nat
v'
+1,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
v': Nat
v'
(
0: Nat
0
::
l: List Nat
l
)
[0, 0, 0]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
0: Nat
0
]
[0, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
]
[0, 1, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
,
1: Nat
1
]
[0, 1, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
0: Nat
0
,
1: Nat
1
,
1: Nat
1
]
[1, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]
[0, 0, 1, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
5: Nat
5
[
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]

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).

def 
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
: (
row: Nat
row
:
Nat: Type
Nat
) (
cols: Nat
cols
:
Nat: Type
Nat
)
List: Type → Type
List
Nat: Type
Nat
|
r: Nat
r
,
c: Nat
c
=>
zero_pad: Nat → List Nat → List Nat
zero_pad
c: Nat
c
(
nat_to_bin: Nat → List Nat
nat_to_bin
r: Nat
r
)
[0, 0, 0, 1, 0, 1]
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
5: Nat
5
6: Nat
6
-- expect [0, 0, 0, 1, 0, 1]

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
def 
bit_to_bool: Nat → Bool
bit_to_bool
:
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
=>
false: Bool
false
| _ =>
true: Bool
true
false
bit_to_bool: Nat → Bool
bit_to_bool
0: Nat
0
-- expect false
true
bit_to_bool: Nat → Bool
bit_to_bool
1: Nat
1
-- expect true

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.

def 
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
:
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Nat
h
::
t: List Nat
t
=> (
bit_to_bool: Nat → Bool
bit_to_bool
h: Nat
h
) :: (
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
t: List Nat
t
) -- expect [false, false, false, true, false, true]
[false, false, false, true, false, true]
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
[
0: Nat
0
,
0: Nat
0
,
0: Nat
0
,
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]

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

def 
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
: (
row: Nat
row
:
Nat: Type
Nat
) (
vars: Nat
vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
r: Nat
r
,
v: Nat
v
=>
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
(
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
r: Nat
r
v: Nat
v
) -- expect [false, false, false, true, false, true]
[false, false, false, true, false, true]
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
5: Nat
5
6: Nat
6

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.

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
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
-- Demonstration def
all_false: Interp
all_false
:
Interp: Type
Interp
:= λ
_: var
_
=>
false: Bool
false
false
all_false: Interp
all_false
v₀: var
v₀
-- expect false
false
all_false: Interp
all_false
v₁: var
v₁
-- expect false
false
all_false: Interp
all_false
v₂: var
v₂
-- expect false -- interp for [false, true, false], i.e., [0, 1, 0] def
interp2: Interp
interp2
:=
override: Interp → var → Bool → Interp
override
all_false: Interp
all_false
v₁: var
v₁
true: Bool
true
false
interp2: Interp
interp2
v₀: var
v₀
-- expect false
true
interp2: Interp
interp2
v₁: var
v₁
-- expect true
false
interp2: Interp
interp2
v₂: var
v₂
-- expect false -- interp for [false, true, true], i.e., [0, 1, 1] def
interp3: Interp
interp3
:=
override: Interp → var → Bool → Interp
override
interp2: Interp
interp2
v₂: var
v₂
true: Bool
true
false
interp3: Interp
interp3
v₀: var
v₀
-- expect false
true
interp3: Interp
interp3
v₁: var
v₁
-- expect true
true
interp3: Interp
interp3
v₂: var
v₂
-- expect true

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.

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
| _, [] =>
all_false: Interp
all_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
-- Demonstration def
interp3': Interp
interp3'
:=
bools_to_interp: List Bool → Interp
bools_to_interp
[
false: Bool
false
,
true: Bool
true
,
true: Bool
true
]
false
interp3': Interp
interp3'
v₀: var
v₀
-- expect false
true
interp3': Interp
interp3'
v₁: var
v₁
-- expect true
true
interp3': Interp
interp3'
v₂: var
v₂
-- expect true

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.

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
) def
interp3'': Interp
interp3''
:=
mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row
3: Nat
3
3: Nat
3
-- vars=3, row=3 -- Demonstration
false
interp3'': Interp
interp3''
v₀: var
v₀
-- expect false
true
interp3'': Interp
interp3''
v₁: var
v₁
-- expect true
true
interp3'': Interp
interp3''
v₂: var
v₂
-- expect true

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!
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
)
0
max_variable_index: Expr → Nat
max_variable_index
{
v₀: var
v₀
}
2
max_variable_index: Expr → Nat
max_variable_index
({
v₀: var
v₀
} {
v₂: var
v₂
}) -- Given expression, return number of variables it assumes 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
/- Generate list of 8 interpretations for three variables -/ def
interps3: List Interp
interps3
:=
mk_interps: Nat → List Interp
mk_interps
3: Nat
3
8
interps3: List Interp
interps3
.
length: {α : Type} → List α → Nat
length
-- expect 8

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 cases
[false, false, false, true]
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
(
mk_interps: Nat → List Interp
mk_interps
2: Nat
2
) ({
v₀: var
v₀
} {
v₁: var
v₁
}) -- [F,F,F,T]
[false, true, true, true]
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
(
mk_interps: Nat → List Interp
mk_interps
2: Nat
2
) ({
v₀: var
v₀
} {
v₁: var
v₁
}) -- [F,T,T,T]

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.

[false, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
)
[false, true, false, true, true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Z: Expr
Z
) -- Write the truth tables on paper then check here
[false, false, false, false, false, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
) (
X: Expr
X
Z: Expr
Z
))
[false, false, false, true, true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
) (
X: Expr
X
Z: Expr
Z
)) -- Study expression and predict outputs before looking. -- What names would you give to these particular propositions?
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(((¬
X: Expr
X
¬
Y: Expr
Y
) ¬(
X: Expr
X
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(((¬
X: Expr
X
¬
Y: Expr
Y
) ¬(
X: Expr
X
Y: Expr
Y
)))
[true, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
))

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

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
=>
or: Bool → Bool → Bool
or
h: Bool
h
(
reduce_and: List Bool → Bool
reduce_and
t: List Bool
t
) 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
) -- A few tests
true
is_valid: Expr → Bool
is_valid
(
X: Expr
X
) -- expect false
true
is_sat: Expr → Bool
is_sat
(
X: Expr
X
) -- exect true
false
is_sat: Expr → Bool
is_sat
(
X: Expr
X
¬
X: Expr
X
) -- expect false #eval
Error: unknown identifier 'is_unsat'
(X ∧ ¬X): ?m.180690
(X ¬X)
-- expect true
true
is_valid: Expr → Bool
is_valid
(
X: Expr
X
¬
X: Expr
X
) -- expect true
true
is_valid: Expr → Bool
is_valid
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))) -- expect true
true
is_valid: Expr → Bool
is_valid
(¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)) -- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: type expected, got (X : Expr)
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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

-- Tests
Sum.inl fun v => Decidable.rec (fun h => false) (fun h => true) (Bool.rec (isFalse (_ : false = true Bool.noConfusionType False false true)) (isTrue (_ : true = true)) ((Nat.rec { fst := fun x => Nat.rec (fun x => true) (fun n n_ih x => false) x PUnit.unit, snd := PUnit.unit } (fun n n_ih => { fst := fun x => Nat.rec (fun x => false) (fun n_1 n_ih x => x.1.1 n_1) x { fst := n_ih, snd := PUnit.unit }, snd := { fst := n_ih, snd := PUnit.unit } }) v.1).1 0))
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
) -- expect Sum.inl _ (a function)
Sum.inr PUnit.unit
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
X: Expr
X
) -- expect Sum.inr Unit.unit -- List of Booleans for first *num_vars* variables under given Interp def
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
:
Interp: Type
Interp
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
| _,
0: Nat
0
=>
[]: List Bool
[]
|
i: Interp
i
, (
n': Nat
n'
+ 1) =>
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
i: Interp
i
n': Nat
n'
++ [(
i: Interp
i
(
var.mk: Nat → var
var.mk
n': Nat
n'
))]

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.

def 
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
:
SomeModelOrNone: Type
SomeModelOrNone
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
Sum.inl: {α : Type ?u.194322} → {β : Type ?u.194321} → α → α ⊕ β
Sum.inl
i: Interp
i
,
n: Nat
n
=>
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
i: Interp
i
n: Nat
n
|
Sum.inr: {α : Type ?u.194352} → {β : Type ?u.194351} → β → α ⊕ β
Sum.inr
_, _ =>
[]: List Bool
[]
-- Test cases
[true, false]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
Y: Expr
Y
))
2: Nat
2
[]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
X: Expr
X
))
2: Nat
2
[true, false]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(¬
X: Expr
X
¬
Y: Expr
Y
))
2: Nat
2
-- list of all models, then convert to list of lists of bools?

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.map : {α : Type u_1} {β : Type u_2} β) List α List β
@
List.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β
List.map
[1, 2, 3, 4, 5]
List.map: {α β : Type} → (α → β) → List α → List β
List.map
(λ
n: Nat
n
=>
n: Nat
n
+
1: Nat
1
) [
0: Nat
0
,
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
]
[1, 4, 6]
List.map: {α β : Type} → (α → β) → List α → List β
List.map
String.length: String → Nat
String.length
[
"I": String
"I"
,
"Love": String
"Love"
,
"Logic!": String
"Logic!"
]

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.foldr : {α : Type u_1} {β : Type u_2} β β) β List α β
@
List.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β
List.foldr
15
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.add: Nat → Nat → Nat
Nat.add
0: Nat
0
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 15
0
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.mul: Nat → Nat → Nat
Nat.mul
0: Nat
0
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 120, oops!
120
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.mul: Nat → Nat → Nat
Nat.mul
1: Nat
1
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 120, ah!

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.filter : {α : Type u_1} Bool) List α List α
@
List.filter: {α : Type u_1} → (α → Bool) → List α → List α
List.filter
[0, 2, 4, 6]
List.filter: {α : Type} → (α → Bool) → List α → List α
List.filter
(λ (
n: Nat
n
:
Nat: Type
Nat
) =>
n: Nat
n
%
2: Nat
2
==
0: Nat
0
) [
0: Nat
0
,
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
,
6: Nat
6
,
7: Nat
7
]

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
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'
)) -- REVIEW
[[]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
0: Nat
0
[[false], [true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
1: Nat
1
[[false, false], [false, true], [true, false], [true, true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
2: Nat
2
[[false, false, false], [false, false, true], [false, true, false], [false, true, true], [true, false, false], [true, false, true], [true, true, false], [true, true, true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
3: Nat
3

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

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 : Type u_1 Type u_1
@
Option: Type u_1 → Type u_1
Option
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
-- REVIEW -- Utility: convert a "Option model" into a list of Bools, empty for none def
some_model_or_none_to_bools: SomeInterpOrNone → Nat → List Bool
some_model_or_none_to_bools
:
SomeInterpOrNone: Type
SomeInterpOrNone
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
Sum.inl: {α : Type ?u.57773} → {β : Type ?u.57772} → α → α ⊕ β
Sum.inl
i: Interp
i
,
n: Nat
n
=>
interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool
n: Nat
n
i: Interp
i
|
Sum.inr: {α : Type ?u.57803} → {β : Type ?u.57802} → β → α ⊕ β
Sum.inr
_, _ =>
[]: 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

def 
X: Expr
X
:= {
var.mk: Nat → var
var.mk
0: Nat
0
} def
Y: Expr
Y
:= {
var.mk: Nat → var
var.mk
1: Nat
1
} def
Z: Expr
Z
:= {
var.mk: Nat → var
var.mk
2: Nat
2
}
[false, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
)
true
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
or: Bool → Bool → Bool
or
false: Bool
false
(
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
))
false
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
and: Bool → Bool → Bool
and
true: Bool
true
(
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
))

Is it true that if X being true makes Y true, then does X being false make Y false?

Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not X) (Expr.un_exp unary_op.not Y)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
[[false, true]]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
(
implies: Bool → Bool → Bool
implies
(
implies: Bool → Bool → Bool
implies
false: Bool
false
true: Bool
true
) (
implies: Bool → Bool → Bool
implies
true: Bool
true
false: Bool
false
))

Is it true that if X being true means that Y must be true, then does Y being false imply X is false?

Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not Y) (Expr.un_exp unary_op.not X)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
true
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
[]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))

We can find all the models of an expression.

[[false, false], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))

Simple model counting.

3
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
)
1
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
)

Search for models (returns list of functions)

[]
find_models: Expr → List Interp
find_models
(
X: Expr
X
¬
X: Expr
X
) -- expect []
3
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 3
1
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 1

Search for models (returns list of list of bools)

[]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- []
[[false], [true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- [[false], [true]
[[true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
Y: Expr
Y
) -- [[true, true]]
[[false, false], [false, true], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(¬(
X: Expr
X
Y: Expr
Y
) ¬
X: Expr
X
¬
Y: Expr
Y
) -- all four interps
[[false, false, false], [false, false, true], [false, true, false], [false, true, true], [true, false, false], [true, false, true], [true, true, false], [true, true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (
Y: Expr
Y
Z: Expr
Z
) (
X: Expr
X
Z: Expr
Z
)) -- all eight interps

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.map : {α : Type u_1} {β : Type u_2} β) List α List β
@
List.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β
List.map
@List.foldr : {α : Type u_1} {β : Type u_2} β β) β List α β
@
List.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β
List.foldr
@List.filter : {α : Type u_1} Bool) List α List α
@
List.filter: {α : Type u_1} → (α → Bool) → List α → List α
List.filter

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

def 
X: Expr
X
:= {
var.mk: Nat → var
var.mk
0: Nat
0
} def
Y: Expr
Y
:= {
var.mk: Nat → var
var.mk
1: Nat
1
} def
Z: Expr
Z
:= {
var.mk: Nat → var
var.mk
2: Nat
2
} -- If X being true makes Y true, then does X being false make Y false?
Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not X) (Expr.un_exp unary_op.not Y)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
[[false, true]]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
(
implies: Bool → Bool → Bool
implies
(
implies: Bool → Bool → Bool
implies
false: Bool
false
true: Bool
true
) (
implies: Bool → Bool → Bool
implies
true: Bool
true
false: Bool
false
)) -- If X implies Y, then does not Y false imply not X?
Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not Y) (Expr.un_exp unary_op.not X)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
true
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
[]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
)) -- Find all the models of an expression.
[[false, false], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)) -- Simple model counting.
3
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
)
1
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
) -- Find all models, return list of functions
[]
find_models: Expr → List Interp
find_models
(
X: Expr
X
¬
X: Expr
X
) -- expect []
3
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 3
1
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 1 -- Find all models, returns list of bool lists
[]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- []
[[false], [true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- [[false], [true]
[[true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
Y: Expr
Y
) -- [[true, true]]
[[false, false], [false, true], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(¬(
X: Expr
X
Y: Expr
Y
) ¬
X: Expr
X
¬
Y: Expr
Y
) -- all four interps
[[false, false, false], [false, false, true], [false, true, false], [false, true, true], [true, false, false], [true, false, true], [true, true, false], [true, true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (
Y: Expr
Y
Z: Expr
Z
) (
X: Expr
X
Z: Expr
Z
)) -- all eight interps

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.

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.

Empty : Type
Empty: Type
Empty

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.

False : Prop
False: Prop
False

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 : Type
Unit: Type
Unit
-- inductive PUnit : Sort u where -- | unit : PUnit

True

True : Prop
True: Prop
True

inductive True : Prop where | intro : True

True.intro : True
True.intro: True
True.intro
-- no elimination rule def
proof_of_true: True
proof_of_true
:
True: Prop
True
:=
True.intro: True
True.intro

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.{u, v} (α : Type u) (β : Type v) : Type (max u v)
Prod: Type u → Type v → Type (max u v)
Prod
/- structure Prod (α : Type u) (β : Type v) where fst : α snd : β -/

And

And (a b : Prop) : Prop
And: Prop → Prop → Prop
And
/- structure And (a b : Prop) : Prop where intro :: left : a right : b -/ -- Propositions as types, proofs as values inductive
Birds_chirping: Prop
Birds_chirping
:
Prop: Type
Prop
|
yep: Birds_chirping
yep
|
boo: Birds_chirping
boo
-- Propositions as types, proofs as values inductive
Sky_blue: Prop
Sky_blue
:
Prop: Type
Prop
|
yep: Sky_blue
yep
Birds_chirping Sky_blue : Prop
(
And: Prop → Prop → Prop
And
Birds_chirping: Prop
Birds_chirping
Sky_blue: Prop
Sky_blue
)
Birds_chirping Sky_blue : Prop
(
Birds_chirping: Prop
Birds_chirping
Sky_blue: Prop
Sky_blue
) theorem
a_proof: Birds_chirping ∧ Sky_blue
a_proof
:
Birds_chirping: Prop
Birds_chirping
Sky_blue: Prop
Sky_blue
:= -- And.intro Birds_chirping.yep Sky_blue.yep
Birds_chirping.yep: Birds_chirping
Birds_chirping.yep
,
Sky_blue.yep: Sky_blue
Sky_blue.yep
-- notation

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.

theorem 
proof_equal: Birds_chirping.boo = Birds_chirping.yep
proof_equal
:
Birds_chirping.boo: Birds_chirping
Birds_chirping.boo
=
Birds_chirping.yep: Birds_chirping
Birds_chirping.yep
:=

Goals 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.{u, v} (α : Type u) (β : Type v) : Type (max u v)
Sum: Type u → Type v → Type (max u v)
Sum
/- inductive Sum (α : Type u) (β : Type v) where | inl (val : α) : Sum α β | inr (val : β) : Sum α β -/

Or Connective

Or (a b : Prop) : Prop
Or: Prop → Prop → Prop
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, theorem
one_or_other: Birds_chirping ∨ Sky_blue
one_or_other
:
Or: Prop → Prop → Prop
Or
Birds_chirping: Prop
Birds_chirping
Sky_blue: Prop
Sky_blue
:=
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
Birds_chirping.yep: Birds_chirping
Birds_chirping.yep
theorem
one_or_other': Birds_chirping ∨ Sky_blue
one_or_other'
:
Or: Prop → Prop → Prop
Or
Birds_chirping: Prop
Birds_chirping
Sky_blue: Prop
Sky_blue
:=
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
Sky_blue.yep: Sky_blue
Sky_blue.yep
example: one_or_other = one_or_other'
example
:
one_or_other: Birds_chirping ∨ Sky_blue
one_or_other
=
one_or_other': Birds_chirping ∨ Sky_blue
one_or_other'
:=
rfl: ∀ {α : Prop} {a : α}, a = a
rfl
-- the different proofs are equal

In some cases you'll need to select the disjuct for which you have a proof.

example: Birds_chirping ∨ 0 = 1
example
:
Or: Prop → Prop → Prop
Or
Birds_chirping: Prop
Birds_chirping
(
0: Nat
0
=
1: Nat
1
) :=
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
Birds_chirping.yep: Birds_chirping
Birds_chirping.yep
-- inr is no go
example: 0 = 1 ∨ 1 = 2
example
: (
0: Nat
0
=
1: Nat
1
) (
1: Nat
1
=
2: Nat
2
):=
Error: don't know how to synthesize placeholder context: 0 = 1 1 = 2
-- 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.

Not (a : Prop) : Prop
Not: Prop → Prop
Not

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 Type

Examples 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: ∀ (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
) |
nab: ¬(A ∧ B)
nab
=>
Error: don't know how to synthesize placeholder context: A B : Prop x : ¬(A B) nab : ¬(A B) := x ¬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 := 
Error: don't know how to synthesize placeholder context: X : Prop 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 := 
Error: don't know how to synthesize placeholder context: Prop
em P : P ¬P
em: ∀ (P : Prop), P ∨ ¬P
em
P: Prop
P

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.

em (P : Prop) : P ¬P
em: ∀ (P : Prop), P ∨ ¬P
em
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
:= λ
nab: ¬(A ∧ B)
nab
=> let
proof_of_aornota: A ∨ ¬A
proof_of_aornota
:=
em: ∀ (P : Prop), P ∨ ¬P
em
A: Prop
A
let
proof_of_bornotb: B ∨ ¬B
proof_of_bornotb
:=
em: ∀ (P : Prop), P ∨ ¬P
em
B: Prop
B
match
proof_of_aornota: A ∨ ¬A
proof_of_aornota
with |
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
a: A
a
=> match
proof_of_bornotb: B ∨ ¬B
proof_of_bornotb
with |
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
b: B
b
=>
False.elim: ∀ {C : Prop}, False → C
False.elim
(
nab: ¬(A ∧ B)
nab
(
And.intro: ∀ {a b : Prop}, a → b → a ∧ b
And.intro
a: A
a
b: B
b
)) |
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
nb: ¬B
nb
=>
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
nb: ¬B
nb
|
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
na: ¬A
na
=>
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
na: ¬A
na
-- 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 % 2 = 0 : Prop
4: Nat
4
%
2: Nat
2
=
0: Nat
0

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.

def 
is_even: Nat → Prop
is_even
:
Nat: Type
Nat
Prop: Type
Prop
:= λ
n: Nat
n
=>
n: Nat
n
%
2: Nat
2
=
0: Nat
0
is_even 4 : Prop
is_even: Nat → Prop
is_even
4: Nat
4
0 = 0
is_even: Nat → Prop
is_even
4: Nat
4
is_even 5 : Prop
is_even: Nat → Prop
is_even
5: Nat
5

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 Prop
(
is_even: Nat → Prop
is_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_even 4 : Prop
is_even: Nat → Prop
is_even
4: Nat
4
is_even 5 : Prop
is_even: Nat → Prop
is_even
5: Nat
5

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.

0 = 0
is_even: Nat → Prop
is_even
0: Nat
0
-- ✓
1 = 0
is_even: Nat → Prop
is_even
1: Nat
1
-- ×
0 = 0
is_even: Nat → Prop
is_even
2: Nat
2
-- ✓
1 = 0
is_even: Nat → Prop
is_even
3: Nat
3
-- ×
0 = 0
is_even: Nat → Prop
is_even
4: Nat
4
-- ✓
1 = 0
is_even: Nat → Prop
is_even
5: Nat
5
-- ×

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.

def 
evens: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
evens
:
Set: ?m.355
Set
Nat := {
n: ?m.371
n
| is_even n }
sorryAx ?m.545 true
evens: {x : Sort ?u.537} → {Set : x} → sorryAx (Sort ?u.536) true
evens
4
sorryAx ?m.557 true
evens: {x : Sort ?u.549} → {Set : x} → sorryAx (Sort ?u.548) true
evens
5

Predicates of Multiple Arguments

Predicates can take any number of arguments. Here are some examples.

Ordered pairs of numbers and their squares

def 
square_pair: Nat × Nat → Prop
square_pair
:
Nat: Type
Nat
×
Nat: Type
Nat
Prop: Type
Prop
| (
n1: Nat
n1
,
n2: Nat
n2
) =>
n2: Nat
n2
=
n1: Nat
n1
^
2: Nat
2
1 = 1
square_pair: Nat × Nat → Prop
square_pair
(
1: Nat
1
,
1: Nat
1
) -- ✓
4 = 4
square_pair: Nat × Nat → Prop
square_pair
(
2: Nat
2
,
4: Nat
4
) -- ✓
9 = 9
square_pair: Nat × Nat → Prop
square_pair
(
3: Nat
3
,
9: Nat
9
) -- ✓
20 = 25
square_pair: Nat × Nat → Prop
square_pair
(
5: Nat
5
,
20: Nat
20
) -- × def
square_pairs: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
square_pairs
:
Set: ?m.877
Set
(Nat × Nat) := {
p: Nat × Nat
p
:
Nat: Type
Nat
×
Nat: Type
Nat
| square_pair p }
sorryAx ?m.1064 true
square_pairs: {x : Sort ?u.1056} → {Set : x} → sorryAx (Sort ?u.1055) true
square_pairs
(3, 9)
sorryAx ?m.1076 true
square_pairs: {x : Sort ?u.1068} → {Set : x} → sorryAx (Sort ?u.1067) true
square_pairs
(3, 10)
sorryAx ?m.1115 true
(
3: Nat
3
,
9: Nat
9
)
square_pairs: {x : Sort ?u.1107} → {Set : x} → sorryAx (Type ?u.1079) true
square_pairs

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.

def 
square_pair': Nat → Nat → Prop
square_pair'
:
Nat: Type
Nat
Nat: Type
Nat
Prop: Type
Prop
|
n1: Nat
n1
,
n2: Nat
n2
=>
n2: Nat
n2
=
n1: Nat
n1
^
2: Nat
2
1 = 1
square_pair': Nat → Nat → Prop
square_pair'
1: Nat
1
1: Nat
1
-- ✓
4 = 4
square_pair': Nat → Nat → Prop
square_pair'
2: Nat
2
4: Nat
4
-- ✓
9 = 9
square_pair': Nat → Nat → Prop
square_pair'
3: Nat
3
9: Nat
9
-- ✓
20 = 25
square_pair': Nat → Nat → Prop
square_pair'
5: Nat
5
20: Nat
20
-- × def
square_pairs': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
square_pairs'
:
Set: ?m.1425
Set
(Nat × Nat) := {
p: Nat × Nat
p
:
Nat: Type
Nat
×
Nat: Type
Nat
| square_pair' p.1 p.2 }
sorryAx ({x : Sort u_1} {Set : x} sorryAx (Sort u_2) true) true
square_pairs: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
square_pairs
sorryAx ?m.1649 true
(
3: Nat
3
,
9: Nat
9
)
square_pairs: {x : Sort ?u.1641} → {Set : x} → sorryAx (Type ?u.1613) true
square_pairs

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

def 
pythagorean_triple: Nat → Nat → Nat → Prop
pythagorean_triple
:
Nat: Type
Nat
Nat: Type
Nat
Nat: Type
Nat
Prop: Type
Prop
|
h: Nat
h
,
x: Nat
x
,
y: Nat
y
=>
h: Nat
h
^
2: Nat
2
=
x: Nat
x
^
2: Nat
2
+
y: Nat
y
^
2: Nat
2
25 = 25
pythagorean_triple: Nat → Nat → Nat → Prop
pythagorean_triple
5: Nat
5
4: Nat
4
3: Nat
3
def
py_trips: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
py_trips
:
Set: ?m.2035
Set
(Nat × Nat × Nat) := {
t: ?m.2051
t
| t.1^2 = t.2.1^2 + t.2.2^2}
sorryAx ?m.2221 true
py_trips: {x : Sort ?u.2213} → {Set : x} → sorryAx (Sort ?u.2212) true
py_trips
(5,4,3)

Homework

(1) Define a predicate, ev_len_str, expressing the property of a string of being of an even-length.

-- Here

def 
ev_len_str: String → Prop
ev_len_str
:
String: Type
String
Prop: Type
Prop
|
s: String
s
=>
s: String
s
.
length: String → Nat
length
%
2: Nat
2
=
0: Nat
0
/- (2) Use #check to typecheck an expression for the set of all even length strings. -/ #check {
s: String
s
:
String: Type
String
| 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. -/ def
str_eq_len: String → Nat → Prop
str_eq_len
:
String: Type
String
Nat: Type
Nat
Prop: Type
Prop
|
s: String
s
,
n: Nat
n
=>
s: String
s
.
length: String → Nat
length
=
n: Nat
n
-- 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 def
str_eq_lens: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
str_eq_lens
:
Set: ?m.2407
Set
(String × Nat) := {
p: ?m.2423
p
| 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. -/ -- Here
example: sorryAx Prop true
example
:
Error: invalid constructor ⟨...⟩, expected type must be an inductive type ?m.2587
str_eq_lens: {x : Sort ?u.2598} → {Set : x} → sorryAx (Type ?u.2585) true
str_eq_lens
:=
rfl: ∀ {α : Sort ?u.2606} {a : α}, a = a
rfl
/- (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. -/ -- Here
example: ¬sorryAx Prop true
example
:
Error: invalid constructor ⟨...⟩, expected type must be an inductive type ?m.2725
str_eq_lens: {x : Sort ?u.2730} → {Set : x} → sorryAx (Type ?u.2723) true
str_eq_lens
:= λ (
t: sorryAx Prop true
t
:
Error: invalid constructor ⟨...⟩, expected type must be an inductive type ?m.2741
str_eq_lens: {x : Sort ?u.2752} → {Set : x} → sorryAx (Type ?u.2739) true
str_eq_lens
) =>
Error: missing cases: _
t: sorryAx Prop true
t

(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.

-- Here

Quantifiers: 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: ∀ {ℕ : Sort u_1}, ℕ → True
example
: (
Warning: unused variable `n` [linter.unusedVariables]
:
: Sort u_1
),
True: Prop
True
:= fun
Warning: unused variable `n` [linter.unusedVariables]
=>
True.intro: True
True.intro

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 True : Prop
(
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
),
True: Prop
True
-- Literally Nat → True!

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.

def 
square: Nat → Nat
square
: (
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
),
Nat: Type
Nat
:= λ
n: Nat
n
=>
n: Nat
n
^
2: Nat
2
square : Nat Nat
(
square: Nat → Nat
square
) -- Nat → Nat
25
square: Nat → Nat
square
5: Nat
5
-- 25

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.

def 
fimpf: False → False
fimpf
: (
Warning: unused variable `f` [linter.unusedVariables]
:
False: Prop
False
),
False: Prop
False
:= λ
f: False
f
=> nomatch
f: False
f
fimpf : False False
(
fimpf: False → False
fimpf
) -- a value/proof of type 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 : Type
(
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
),
Nat: Type
Nat
(d : Dog), Blue d : Prop
(
d: Dog
d
:
Dog: Type
Dog
),
Blue: Dog → Prop
Blue
d: Dog
d

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 Bool : Type
(
Warning: unused variable `n` [linter.unusedVariables]
:
Nat: Type
Nat
),
Bool: Type
Bool
-- function type, Nat → Bool

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.

def 
is_even: Nat → Prop
is_even
:
Nat: Type
Nat
Prop: Type
Prop
:= fun
n: Nat
n
=>
n: Nat
n
%
2: Nat
2
=
0: Nat
0
-- Each of these propositions is a different type in Lean
is_even 0 : Prop
is_even: Nat → Prop
is_even
0: Nat
0
is_even 1 : Prop
is_even: Nat → Prop
is_even
1: Nat
1
is_even 2 : Prop
is_even: Nat → Prop
is_even
2: Nat
2
is_even 3 : Prop
is_even: Nat → Prop
is_even
3: Nat
3

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 : Nat), is_even n : Prop
(
n: Nat
n
:
Nat: Type
Nat
),
is_even: Nat → Prop
is_even
n: Nat
n

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 : String
s: String
s
-- 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 P
fa : (x : T), P x
fa: ∀ (x : T), P x
fa
-- ∀ (x : α), P x -- Therefore *t* in particular satisfies P -- We obtain a proof by simply applying fa to t
fa t : P t
fa: ∀ (x : T), P x
fa
t: T
t
-- P t end decls

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_blue Iris : Blue Iris
all_dogs_blue: ∀ (d : Dog), Blue d
all_dogs_blue
Iris: Dog
Iris
-- universal specialization end bluedog

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: Type
Person
:
Type: Type 1
Type
) (
Socrates: Person
Socrates
:
Person: Type
Person
) (
Mortal: Person → Prop
Mortal
:
Person: Type
Person
Prop: Type
Prop
) (
everyone_is_mortal: ∀ (p : Person), Mortal p
everyone_is_mortal
: (
p: Person
p
:
Person: Type
Person
),
Mortal: Person → Prop
Mortal
p: Person
p
)
everyone_is_mortal Socrates : Mortal Socrates
everyone_is_mortal: ∀ (p : Person), Mortal p
everyone_is_mortal
Socrates: Person
Socrates
end socrates

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, is_even n : Prop
(
n: Nat
n
:
Nat: Type
Nat
),
is_even: Nat → Prop
is_even
n: Nat
n

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).

1 = 0 1 = 1 1 = 2 1 = 3 1 = 4
small: Nat → Prop
small
1: Nat
1

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: small 1
example
:
small: Nat → Prop
small
1: Nat
1
:= (
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
(
Error: don't know how to synthesize placeholder for argument 'h' context: 1 = 1 1 = 2 1 = 3 1 = 4
))

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.

def 
s1: {x : Sort u_1} → {Set : x} → sorryAx (Type u_2) true
s1
:
Set: ?m.204
Set
Nat :=
{: sorryAx (Type u_2) true
{
0, 1, 2, 3, 4 }
sorryAx (sorryAx (Type u_2) true) true
s1: {x : Sort u_1} → {Set : x} → sorryAx (Type u_2) true
s1
-- the predicate that represents this set

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.

sorryAx ({x : Sort u_1} {Set : x} sorryAx (Sort u_2) true) true
s2: {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
s2

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.

def 
ev_set': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
ev_set'
:
Error: function expected at Set term has type ?m.461
:=
ev: Nat → Prop
ev
-- ev is a predicate def
small_set': {x : Sort u_1} → {Set : x} → sorryAx (Sort u_2) true
small_set'
:
Error: function expected at Set term has type ?m.633
:=
small: Nat → Prop
small
-- small is too

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.

#check 
1: Nat
1
small_set: {x : Sort ?u.1172} → {Set : x} → sorryAx (Type ?u.1158) true
small_set
-- membership proposition in set theory
sorryAx ?m.1212 true
1: Nat
1
small_set: {x : Sort ?u.1204} → {Set : x} → sorryAx (Type ?u.1190) true
small_set
-- this proposition in predicate logic
Warning: declaration uses 'sorry'
:
1: Nat
1
small_set: {x : Sort ?u.1245} → {Set : x} → sorryAx (Type ?u.1225) true
small_set
:=
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
(
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
rfl: ∀ {α : Sort ?u.1268} {a : α}, a = a
rfl
) -- a proof of it

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 _.

sorryAx ?m.1406 true
3: Nat
3
small_set: {x : Sort ?u.1398} → {Set : x} → sorryAx (Type ?u.1384) true
small_set
Warning: declaration uses 'sorry'
:
3: Nat
3
small_set: {x : Sort ?u.1439} → {Set : x} → sorryAx (Type ?u.1419) true
small_set
:=
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
(
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
(
Or.inr: ∀ {a b : Prop}, b → a ∨ b
Or.inr
(
Or.inl: ∀ {a b : Prop}, a → a ∨ b
Or.inl
rfl: ∀ {α : Sort ?u.1466} {a : α}, a = a
rfl
)))

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 
Error: unknown namespace 'Set'
sorryAx ?m.1583 true
Error: unknown identifier 'univ'
-- fun _a => True
sorryAx ?m.1587 true
Error: unknown identifier 'univ'
0: ?m.1587
0
-- True
sorryAx ?m.1591 true
Error: unknown identifier 'univ'
123456: ?m.1591
123456
-- True

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.

sorryAx ?m.1595 true
Error: unknown identifier 'Set.inter'
-- fun s₁ s₂ a => s₁ a ∧ s₂ a variable (α : Type) (s t : Set α)
s : sorryAx (Sort ?u.1620) true
s: sorryAx (Sort ?u.1620) true
s
t -- the intersection of sets is a set
s
s: sorryAx (Sort ?u.1655) true
s
t -- its membership predicate is formed using ∧

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.

Error: invalid occurrence of universe level 'u_2' at 'even_and_small_set', it does not occur at the declaration type, nor it is explicit universe level provided by the user, occurring at expression ev_set.{u_2, u_1} at declaration body ev_set
small_set -- intersection!
?m.1763.1 0 (sorryAx ?m.1762 true)
(
0: Nat
0
Error: unknown identifier 'even_and_small_set'
) -- 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.

?m.1856.1 6 (sorryAx ?m.1855 true)
6: Nat
6
Error: unknown identifier 'even_and_small_set'
-- to prove: 0 = 0 ∧ (6 = 0 ∨ 6 = 1 ∨ 6 = 2 ∨ 6 = 3 ∨ 6 = 4) example:
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.1966
:= _

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: 
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.2087
:= ⟨ _, _ ⟩

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: 
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.2208
:= ⟨ 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 : 
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.2323
:= 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.

sorryAx ?m.2420 true
Error: invalid use of field notation with `@` modifier
-- fun {α} s₁ s₂ a => s₁ a ∨ s₂ a
Error: invalid occurrence of universe level 'u_2' at 'even_or_small_set', it does not occur at the declaration type, nor it is explicit universe level provided by the user, occurring at expression ev_set.{u_2, u_1} at declaration body ev_set
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.

?m.2504.1 3 (sorryAx ?m.2503 true)
3: Nat
3
Error: unknown identifier 'even_or_small_set'

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 : 
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.2614
:= Or.inr _ example :
Error: typeclass instance problem is stuck, it is often due to metavariables Membership Nat ?m.2735
:= _
example: ¬sorryAx Prop true
example
:
7: Nat
7
ev_set: {x : Sort ?u.2850} → {Set : x} → sorryAx (Type ?u.2836) true
ev_set
small_set := _
example: sorryAx Prop true
example
:
7: Nat
7
ev_set: {x : Sort ?u.2923} → {Set : x} → sorryAx (Type ?u.2903) true
ev_set
:=
Error: don't know how to synthesize placeholder context: Set : ?m.2871 α : Type s : sorryAx (Sort ?u.2875) true t : sorryAx (Sort ?u.2878) true x : Sort ?u.2870 Set : x α : Type s : sorryAx (Sort ?u.2875) true t : sorryAx (Sort ?u.2878) true sorryAx Prop true
-- stuck example : 7 ev_set := λ h =>
Error: don't know how to synthesize placeholder context: Set : ?m.2945 α : Type s : sorryAx (Sort ?u.2949) true t : sorryAx (Sort ?u.2952) true x : Sort ?u.2944 Set : x α : Type s : sorryAx (Sort ?u.2949) true t : sorryAx (Sort ?u.2952) true h : sorryAx Prop true False

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
s: sorryAx (Sort ?u.3022) true
s
-- fun x => x ∈ s → False means fun x => x ∉ s -- fun x => x ∈ s → False variable (
s: sorryAx (Sort ?u.3084) true
s
:
Error: function expected at Set term has type x
)
s : sorryAx (Sort ?u.3104) true
s: sorryAx (Sort ?u.3104) true
s
-- Standard notation for complement of set s

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

sorryAx ?m.3270 true
Error: invalid field notation, type is not of the form (C ...) where C is a constant Set has type x
-- 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 ⟩
sorryAx ?m.3427 true
6: Nat
6
ev_set: {x : Sort ?u.3419} → {Set : x} → sorryAx (Type ?u.3405) true
ev_set
\ small_set

Subset

sorryAx ?m.3487 true
Error: invalid use of field notation with `@` modifier
-- fun {α} s₁ s₂ => ∀ ⦃a : α⦄, a ∈ s₁ → s₂ a

Powerset

sorryAx ?m.3538 true
Error: invalid use of field notation with `@` modifier
-- fun {α} s t => ∀ ⦃a : α⦄, a ∈ t → s a

Set Theory and Logical Underpinnings

Set Theory ConceptSet Theory DefinitionConstructive Logic Reduction (Lean)
set αaxioms of set theorypredicate (α → 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 
Error: don't know how to synthesize placeholder context: ?m.2
-- 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 Bool
(
isEven: Nat → Bool
isEven
) -- Nat → Bool
true
isEven: Nat → Bool
isEven
2: Nat
2
-- expect true
false
isEven: Nat → Bool
isEven
3: Nat
3
-- expect false

Define your function here. When you've got it right, the following test cases should pass.

-- Now complete the implementation of glue_funs'
def 
glue_funs': (x : ?m.5416) → ?m.5417 x
glue_funs'
:
Error: failed to infer definition type when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
| _ => _ #eval
Error: unknown identifier 'glue_funs''
isEven String.length "Hello": ?m.5432
isEven String.length "Hello"
-- false #eval
Error: unknown identifier 'glue_funs''
isEven String.length "Hello!": ?m.5451
isEven String.length "Hello!"
-- true

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
def 
glue_funs: (x : ?m.5481) → ?m.5482 x
glue_funs
:
Error: failed to infer definition type when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
| _ => _ -- test cases #eval
Error: unknown identifier 'glue_funs'
isEven String.length "Hello": ?m.5497
isEven String.length "Hello"
-- false #eval
Error: unknown identifier 'glue_funs'
isEven String.length "Hello!": ?m.5516
isEven String.length "Hello!"
-- true

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 
Error: don't know how to synthesize placeholder context: ?m.5535
#eval
Error: don't know how to synthesize placeholder context: ?m.5538

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 × Nat : Type
Nat: Type
Nat
×
Nat: Type
Nat
×
Nat: Type
Nat
Nat × Nat × Nat : Type
Nat: Type
Nat
× (
Nat: Type
Nat
×
Nat: Type
Nat
)
(Nat × Nat) × Nat : Type
(
Nat: Type
Nat
×
Nat: Type
Nat
) ×
Nat: Type
Nat

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.

def 
prod_comm: {α β : Type} → α × β → β × α
prod_comm
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} :
α: Type
α
×
β: Type
β
β: Type
β
×
α: Type
α
| _ =>
Error: don't know how to synthesize placeholder context: α β : Type x : α × β β × α

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} :  α ×× γ) × β) × γ
| _ => 
Error: don't know how to synthesize placeholder context: α β γ : Type x : α × β × γ × β) × γ

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
  | _ => _
  
Error: redundant alternative

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 
Error: don't know how to synthesize placeholder for argument 'val' context: α β γ : Type a : α α β
) | (
Sum.inr: {α : Type ?u.458} → {β : Type ?u.457} → β → α ⊕ β
Sum.inr
(
Sum.inl: {α : Type ?u.463} → {β : Type ?u.462} → α → α ⊕ β
Sum.inl
b: β
b
)) =>
Error: don't know how to synthesize placeholder context: α β γ : Type b : β β) γ
| _ =>
Error: don't know how to synthesize placeholder context: α β γ : Type x : α β γ β) γ

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} : 
Error: failed to infer definition type when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
| _ => _ | _ => _

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.

def 
not_either_not_both: {jam cheese : Type} → no jam ⊕ no cheese → no (jam × cheese)
not_either_not_both
{
jam: Type
jam
cheese: Type
cheese
} : ((
no: Type → Type
no
jam: Type
jam
) (
no: Type → Type
no
cheese: Type
cheese
)) (
no: Type → Type
no
(
jam: Type
jam
×
cheese: Type
cheese
)) |
Sum.inl: {α : Type ?u.38} → {β : Type ?u.37} → α → α ⊕ β
Sum.inl
nojam: no jam
nojam
=> (fun
_: jam × cheese
_
=>
Error: don't know how to synthesize placeholder context: jam cheese : Type nojam : no jam x : jam × cheese Empty
) |
Sum.inr: {α : Type ?u.65} → {β : Type ?u.64} → β → α ⊕ β
Sum.inr
_ =>
Error: don't know how to synthesize placeholder context: jam cheese : Type val : no cheese no (jam × cheese)

#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).

def 
demorgan1: {α β : Type} → (α → Empty) ⊕ (β → Empty) → α × β → Empty
demorgan1
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} : ((
α: Type
α
Empty: Type
Empty
) (
β: Type
β
Empty: Type
Empty
)) (
α: Type
α
×
β: Type
β
Empty: Type
Empty
) | (
Sum.inl: {α : Type ?u.260} → {β : Type ?u.259} → α → α ⊕ β
Sum.inl
noa: α → Empty
noa
) =>
Error: don't know how to synthesize placeholder context: α β : Type noa : α Empty α × β Empty
| (
Sum.inr: {α : Type ?u.292} → {β : Type ?u.291} → β → α ⊕ β
Sum.inr
nob: β → Empty
nob
) =>
Error: don't know how to synthesize placeholder context: α β : Type nob : β Empty α × β 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 β,

def 
demorgan2: {α β : Type} → (α ⊕ β → Empty) → (α → Empty) × (β → Empty)
demorgan2
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} : (
α: Type
α
β: Type
β
Empty: Type
Empty
) ((
α: Type
α
Empty: Type
Empty
) × (
β: Type
β
Empty: Type
Empty
)) |
noaorb: α ⊕ β → Empty
noaorb
=>
Error: don't know how to synthesize placeholder context: α β : Type x : α β Empty noaorb : α β Empty := x Empty) × 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.

def 
demorgan3: {α β : Type} → (α → Empty) × (β → Empty) → α ⊕ β → Empty
demorgan3
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} : ((
α: Type
α
Empty: Type
Empty
) × (
β: Type
β
Empty: Type
Empty
)) ((
α: Type
α
β: Type
β
)
Empty: Type
Empty
) | _ =>
Error: don't know how to synthesize placeholder context: α β : Type x : (α Empty) × Empty) α β Empty

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 cases
sorryAx ?m.776 true
Error: unknown identifier 'pred'
3: ?m.776
3
-- expect 2
sorryAx ?m.780 true
Error: unknown identifier 'pred'
0: ?m.780
0
-- expect 0

#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
#check 
Error: unknown identifier 'mk_doll'
3: ?m.784
3
sorryAx ?m.786 true
Error: unknown identifier 'mk_doll'
3: ?m.786
3

#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 (_).

def 
nat_eq: Nat → Nat → Bool
nat_eq
:
Nat: Type
Nat
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
,
0: Nat
0
=>
true: Bool
true
|
0: Nat
0
,
n': Nat
n'
+ 1 =>
false: Bool
false
|
n': Nat
n'
+ 1,
0: Nat
0
=>
false: Bool
false
| (
n': Nat
n'
+ 1), (
m': Nat
m'
+ 1) =>
Error: don't know how to synthesize placeholder context: n' m' : Nat Bool
-- a few tests
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors

#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') => 
Error: don't know how to synthesize placeholder context: m n' : Nat Nat
-- hint: recursion -- Some test cases
sorryAx (Nat Nat Nat) true 0 0
add: Nat → Nat → Nat
add
0: Nat
0
0: Nat
0
-- expect 0
sorryAx (Nat Nat Nat) true 5 0
add: Nat → Nat → Nat
add
5: Nat
5
0: Nat
0
-- expect 5
sorryAx (Nat Nat Nat) true 0 5
add: Nat → Nat → Nat
add
0: Nat
0
5: Nat
5
-- expect 5
sorryAx (Nat Nat Nat) true 5 4
add: Nat → Nat → Nat
add
5: Nat
5
4: Nat
4
-- expect 9
sorryAx (Nat Nat Nat) true 4 5
add: Nat → Nat → Nat
add
4: Nat
4
5: Nat
5
-- expect 9
sorryAx (Nat Nat Nat) true 5 5
add: Nat → Nat → Nat
add
5: Nat
5
5: Nat
5
-- expect 10

#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.

def 
mul: Nat → Nat → Nat
mul
:
Nat: Type
Nat
Nat: Type
Nat
Nat: Type
Nat
|
m: Nat
m
,
0: Nat
0
=>
0: Nat
0
|
m: Nat
m
, (
Nat.succ: Nat → Nat
Nat.succ
n': Nat
n'
) =>
add: Nat → Nat → Nat
add
(
Error: don't know how to synthesize placeholder context: m n' : Nat Nat
) (
Error: don't know how to synthesize placeholder context: m n' : 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.

def 
sum_f: (Nat → Nat) → Nat → Nat
sum_f
: (
Nat: Type
Nat
Nat: Type
Nat
)
Nat: Type
Nat
Nat: Type
Nat
|
f: Nat → Nat
f
,
0: Nat
0
=>
Error: don't know how to synthesize placeholder context: f : Nat Nat Nat
|
f: Nat → Nat
f
,
n': Nat
n'
+ 1 =>
Error: don't know how to synthesize placeholder context: f : Nat Nat n' : Nat 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.

def 
apply: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
a: α
a
def
apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply_twice
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
(
f: α → α
f
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

def 
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
{
α: Type
α
:
Type: Type 1
Type
} : (
α: Type
α
α: Type
α
)
α: Type
α
Nat: Type
Nat
α: Type
α
|
f: α → α
f
,
a: α
a
,
0: Nat
0
=>
Error: don't know how to synthesize placeholder context: α : Type f : α α a : α α
|
f: α → α
f
,
a: α
a
, (
n': Nat
n'
+ 1) =>
Error: don't know how to synthesize placeholder context: α : Type f : α α a : α n' : Nat α
-- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times
Error: cannot evaluate code because '_eval._lambda_2' uses 'sorry' and/or contains errors
-- expect 4 -- apply "double" to 2 four times
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 32 -- apply "square" to 2 four times
Error: cannot evaluate code because '_eval._lambda_2' uses 'sorry' and/or contains errors
-- 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 Nat
(
[]: List Nat
[]
:
List: Type → Type
List
Nat: Type
Nat
)
[1, 2, 3] : List Nat
1: Nat
1
::[
2: Nat
2
,
3: Nat
3
]
[1, 2, 3] : List Nat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]

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.

def 
first_elt: List Nat → Unit ⊕ Nat
first_elt
:
List: Type → Type
List
Nat: Type
Nat
Unit: Type
Unit
Nat: Type
Nat
| [] =>
Sum.inl: {α β : Type} → α → α ⊕ β
Sum.inl
Unit.unit: Unit
Unit.unit
|
h: Nat
h
::_ =>
Sum.inr: {α β : Type} → β → α ⊕ β
Sum.inr
h: Nat
h
Sum.inl PUnit.unit
first_elt: List Nat → Unit ⊕ Nat
first_elt
[]: List Nat
[]
-- expect Sum.inl unit
Sum.inr 1
first_elt: List Nat → Unit ⊕ Nat
first_elt
[
1: Nat
1
,
2: Nat
2
] -- expect 1 (left)

#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.

def 
len: {α : Type} → List α → Nat
len
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
Nat: Type
Nat
| _ =>
_: Nat
_
Error: redundant alternative
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 0
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 3
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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
| _ => _
Error: redundant alternative
-- Test cases
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect false
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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 => 
Error: don't know how to synthesize placeholder context: h : Bool t : List Bool List Bool
-- hint: use :: to construct answer -- test cases
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- exect []
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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
#eval 
Error: unknown identifier 'countdown'
0: ?m.34137
0
-- expect [0] #eval
Error: unknown identifier 'countdown'
5: ?m.34156
5
-- expect [5,4,3,2,1,0]

#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

def 
concat: {α : Type} → (x : List ?m.34217) → (x_1 : ?m.34201 x) → ?m.34202 x x_1
concat
{
α: Type
α
:
Type: Type 1
Type
} :
Error: failed to infer definition type when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
| [], m => _ | _, _ => _ -- Test cases #eval
Error: unknown identifier 'concat'
[1,2,3] []: ?m.34261
[1,2,3] []
-- expect [1,2,3] #eval
Error: unknown identifier 'concat'
[] [1,2,3]: ?m.34280
[] [1,2,3]
-- expect [1,2,3] #eval
Error: unknown identifier 'concat'
[1,2] [3,4]: ?m.34299
[1,2] [3,4]
-- expect [1,2,3,4]

#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 
Error: unknown identifier 'pure''
"Hi": ?m.34318
"Hi"
-- expect ["Hi"]

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.

def 
apply: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
a: α
a
def
apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply_twice
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
(
f: α → α
f
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

def 
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
{
α: Type
α
:
Type: Type 1
Type
} : (
α: Type
α
α: Type
α
)
α: Type
α
Nat: Type
Nat
α: Type
α
|
Warning: unused variable `f` [linter.unusedVariables]
,
a: α
a
,
0: Nat
0
=>
a: α
a
|
f: α → α
f
,
a: α
a
, (
n': Nat
n'
+ 1) =>
f: α → α
f
(
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
f: α → α
f
a: α
a
n': Nat
n'
) -- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times
4
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
4: Nat
4
-- expect 4 -- apply "double" to 2 four times
32
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
(λ
n: Nat
n
=>
2: Nat
2
*
n: Nat
n
)
2: Nat
2
4: Nat
4
-- expect 32 -- apply "square" to 2 four times
65536
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
(λ
n: Nat
n
=>
n: Nat
n
^
2: Nat
2
)
2: Nat
2
4: Nat
4
-- 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
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 Nat
(
[]: List Nat
[]
:
List: Type → Type
List
Nat: Type
Nat
)
[1, 2, 3] : List Nat
1: Nat
1
::[
2: Nat
2
,
3: Nat
3
]
[1, 2, 3] : List Nat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]

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.

def 
first_elt: List Nat → Unit ⊕ Nat
first_elt
:
List: Type → Type
List
Nat: Type
Nat
Unit: Type
Unit
Nat: Type
Nat
| [] =>
Sum.inl: {α β : Type} → α → α ⊕ β
Sum.inl
Unit.unit: Unit
Unit.unit
|
h: Nat
h
::_ =>
Sum.inr: {α β : Type} → β → α ⊕ β
Sum.inr
h: Nat
h
Sum.inl PUnit.unit
first_elt: List Nat → Unit ⊕ Nat
first_elt
[]: List Nat
[]
-- expect Sum.inl unit
Sum.inr 1
first_elt: List Nat → Unit ⊕ Nat
first_elt
[
1: Nat
1
,
2: Nat
2
] -- expect 1 (left)

#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.

def 
len: {α : Type} → List α → Nat
len
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
Nat: Type
Nat
| [] =>
0: Nat
0
|
Warning: unused variable `h` [linter.unusedVariables]
::
t: List α
t
=>
1: Nat
1
+
len: {α : Type} → List α → Nat
len
t: List α
t
0
@
len: {α : Type} → List α → Nat
len
Nat: Type
Nat
[]: List Nat
[]
-- expect 0
3
len: {α : Type} → List α → Nat
len
[
0: Nat
0
,
1: Nat
1
,
2: Nat
2
] -- expect 3
3
len: {α : Type} → List α → Nat
len
[
"I": String
"I"
,
"Love": String
"Love"
,
"Logic!": String
"Logic!"
] -- 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
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
) -- Test cases
true
reduce_and: List Bool → Bool
reduce_and
[
true: Bool
true
] -- expect true
false
reduce_and: List Bool → Bool
reduce_and
[
false: Bool
false
] -- expect false
true
reduce_and: List Bool → Bool
reduce_and
[
true: Bool
true
,
true: Bool
true
] -- expect true
false
reduce_and: List Bool → Bool
reduce_and
[
false: Bool
false
,
true: Bool
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
map_not
:
List: Type → Type
List
Bool: Type
Bool
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Bool
h
::
t: List Bool
t
=>
not: Bool → Bool
not
h: Bool
h
::
map_not: List Bool → List Bool
map_not
t: List Bool
t
-- hint: use :: to construct answer -- test cases
[]
map_not: List Bool → List Bool
map_not
[]: List Bool
[]
-- exect []
[false, true]
map_not: List Bool → List Bool
map_not
[
true: Bool
true
,
false: Bool
false
] -- 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
def 
countdown: Nat → List Nat
countdown
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
n': Nat
n'
+ 1 => (
n': Nat
n'
+
1: Nat
1
)::
countdown: Nat → List Nat
countdown
n': Nat
n'
-- test cases
[0]
countdown: Nat → List Nat
countdown
0: Nat
0
-- expect [0]
[5, 4, 3, 2, 1, 0]
countdown: Nat → List Nat
countdown
5: Nat
5
-- expect [5,4,3,2,1,0]

#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

def 
concat: {α : Type} → List α → List α → List α
concat
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
List: Type → Type
List
α: Type
α
List: Type → Type
List
α: Type
α
| [],
m: List α
m
=>
m: List α
m
|
h: α
h
::
t: List α
t
,
m: List α
m
=>
h: α
h
::
concat: {α : Type} → List α → List α → List α
concat
t: List α
t
m: List α
m
-- Test cases
[1, 2, 3]
concat: {α : Type} → List α → List α → List α
concat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]
[]: List Nat
[]
-- expect [1,2,3]
[1, 2, 3]
concat: {α : Type} → List α → List α → List α
concat
[]: List Nat
[]
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
] -- expect [1,2,3]
[1, 2, 3, 4]
concat: {α : Type} → List α → List α → List α
concat
[
1: Nat
1
,
2: Nat
2
] [
3: Nat
3
,
4: Nat
4
] -- expect [1,2,3,4]

#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
def 
pure': String → List String
pure'
:
String: Type
String
List: Type → Type
List
String: Type
String
|
s: String
s
=> [
s: String
s
]
["Hi"]
pure': String → List String
pure'
"Hi": String
"Hi"
-- expect ["Hi"]

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 {α β} p => p : {α β : Type} α × β α × β
fun {
α: Type
α
β: Type
β
:
Type: Type 1
Type
} (
p: α × β
p
:
α: Type
α
×
β: Type
β
) =>
p: α × β
p

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).

fun {α β} p => match p with | (a, b) => (b, a) : {α β : Type} α × β β × α
λ {
α: Type
α
β: Type
β
:
Type: Type 1
Type
} (
p: α × β
p
:
α: Type
α
×
β: Type
β
) -- pair object => match
p: α × β
p
with -- analyze p ... | (
a: α
a
,
b: β
b
) => -- ... as (a, b) (
b: β
b
,
a: α
a
) -- return (b,a) -- Application of function. Expect ("Dolly", " Hello ")
("Dolly", " Hello ")
(λ {
α: Type
α
β: Type
β
:
Type: Type 1
Type
} -- function (
p: α × β
p
:
α: Type
α
×
β: Type
β
) => match
p: α × β
p
with | (
a: α
a
,
b: β
b
) => (
b: β
b
,
a: α
a
) ) -- end function (
" Hello ": String
" Hello "
,
"Dolly": String
"Dolly"
) -- argument

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 {α β} x => match x with | (a, b) => (b, a) : {α β : Type} α × β β × α
fun {
Warning: unused variable `α` [linter.unusedVariables]
Warning: unused variable `β` [linter.unusedVariables]
:
Type: Type 1
Type
} -- implicit type arguments ((
a: α
a
,
b: β
b
) : α × β ) -- *destructured pair, (a, b)* => (
b: β
b
,
a: α
a
) -- swap function is now trivial -- And here's an example application
("Dolly", " Hello ")
(λ {
Warning: unused variable `α` [linter.unusedVariables]
Warning: unused variable `β` [linter.unusedVariables]
:
Type: Type 1
Type
} -- Expect ("Dolly", " Hello ") (
a: α
a
,
b: β
b
) => (
b: β
b
,
a: α
a
)) (
" Hello ": String
" Hello "
,
"Dolly": String
"Dolly"
)

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.

def 
apply: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
a: α
a
def
apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply_twice
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
(
f: α → α
f
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

def 
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
{
α: Type
α
:
Type: Type 1
Type
} : (
α: Type
α
α: Type
α
)
α: Type
α
Nat: Type
Nat
α: Type
α
|
f: α → α
f
,
a: α
a
,
0: Nat
0
=>
Error: don't know how to synthesize placeholder context: α : Type f : α α a : α α
|
f: α → α
f
,
a: α
a
, (
n': Nat
n'
+ 1) =>
Error: don't know how to synthesize placeholder context: α : Type f : α α a : α n' : Nat α
-- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times
Error: cannot evaluate code because '_eval._lambda_2' uses 'sorry' and/or contains errors
-- expect 4 -- apply "double" to 2 four times
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 32 -- apply "square" to 2 four times
Error: cannot evaluate code because '_eval._lambda_2' uses 'sorry' and/or contains errors
-- 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 Nat
(
[]: List Nat
[]
:
List: Type → Type
List
Nat: Type
Nat
)
[1, 2, 3] : List Nat
1: Nat
1
::[
2: Nat
2
,
3: Nat
3
]
[1, 2, 3] : List Nat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]

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.

def 
first_elt: List Nat → Unit ⊕ Nat
first_elt
:
List: Type → Type
List
Nat: Type
Nat
Unit: Type
Unit
Nat: Type
Nat
| [] =>
Sum.inl: {α β : Type} → α → α ⊕ β
Sum.inl
Unit.unit: Unit
Unit.unit
|
h: Nat
h
::_ =>
Sum.inr: {α β : Type} → β → α ⊕ β
Sum.inr
h: Nat
h
Sum.inl PUnit.unit
first_elt: List Nat → Unit ⊕ Nat
first_elt
[]: List Nat
[]
-- expect Sum.inl unit
Sum.inr 1
first_elt: List Nat → Unit ⊕ Nat
first_elt
[
1: Nat
1
,
2: Nat
2
] -- expect 1 (left)

#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.

def 
len: {α : Type} → List α → Nat
len
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
Nat: Type
Nat
| _ =>
_: Nat
_
Error: redundant alternative
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 0
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect 3
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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
| _ => _
Error: redundant alternative
-- Test cases
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect false
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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 => 
Error: don't know how to synthesize placeholder context: h : Bool t : List Bool List Bool
-- hint: use :: to construct answer -- test cases
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- exect []
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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
#eval 
Error: unknown identifier 'countdown'
0: ?m.34137
0
-- expect [0] #eval
Error: unknown identifier 'countdown'
5: ?m.34156
5
-- expect [5,4,3,2,1,0]

#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

def 
concat: {α : Type} → (x : List ?m.34217) → (x_1 : ?m.34201 x) → ?m.34202 x x_1
concat
{
α: Type
α
:
Type: Type 1
Type
} :
Error: failed to infer definition type when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed
| [], m => _ | _, _ => _ -- Test cases #eval
Error: unknown identifier 'concat'
[1,2,3] []: ?m.34261
[1,2,3] []
-- expect [1,2,3] #eval
Error: unknown identifier 'concat'
[] [1,2,3]: ?m.34280
[] [1,2,3]
-- expect [1,2,3] #eval
Error: unknown identifier 'concat'
[1,2] [3,4]: ?m.34299
[1,2] [3,4]
-- expect [1,2,3,4]

#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 
Error: unknown identifier 'pure''
"Hi": ?m.34318
"Hi"
-- expect ["Hi"]

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.

def 
apply: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
a: α
a
def
apply_twice: {α : Sort u_1} → {a : α} → (α → α) → α → α
apply_twice
{
Warning: unused variable `a` [linter.unusedVariables]
:
α: Sort u_1
α
} : (
α: Sort u_1
α
α: Sort u_1
α
)
α: Sort u_1
α
α: Sort u_1
α
|
f: α → α
f
,
a: α
a
=>
f: α → α
f
(
f: α → α
f
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

def 
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
{
α: Type
α
:
Type: Type 1
Type
} : (
α: Type
α
α: Type
α
)
α: Type
α
Nat: Type
Nat
α: Type
α
|
Warning: unused variable `f` [linter.unusedVariables]
,
a: α
a
,
0: Nat
0
=>
a: α
a
|
f: α → α
f
,
a: α
a
, (
n': Nat
n'
+ 1) =>
f: α → α
f
(
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
f: α → α
f
a: α
a
n': Nat
n'
) -- Test cases: confirm that expectations are correct -- apply Nat.succ to zero four times
4
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
Nat.succ: Nat → Nat
Nat.succ
0: Nat
0
4: Nat
4
-- expect 4 -- apply "double" to 2 four times
32
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
(λ
n: Nat
n
=>
2: Nat
2
*
n: Nat
n
)
2: Nat
2
4: Nat
4
-- expect 32 -- apply "square" to 2 four times
65536
apply_n: {α : Type} → (α → α) → α → Nat → α
apply_n
(λ
n: Nat
n
=>
n: Nat
n
^
2: Nat
2
)
2: Nat
2
4: Nat
4
-- 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
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 Nat
(
[]: List Nat
[]
:
List: Type → Type
List
Nat: Type
Nat
)
[1, 2, 3] : List Nat
1: Nat
1
::[
2: Nat
2
,
3: Nat
3
]
[1, 2, 3] : List Nat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]

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.

def 
first_elt: List Nat → Unit ⊕ Nat
first_elt
:
List: Type → Type
List
Nat: Type
Nat
Unit: Type
Unit
Nat: Type
Nat
| [] =>
Sum.inl: {α β : Type} → α → α ⊕ β
Sum.inl
Unit.unit: Unit
Unit.unit
|
h: Nat
h
::_ =>
Sum.inr: {α β : Type} → β → α ⊕ β
Sum.inr
h: Nat
h
Sum.inl PUnit.unit
first_elt: List Nat → Unit ⊕ Nat
first_elt
[]: List Nat
[]
-- expect Sum.inl unit
Sum.inr 1
first_elt: List Nat → Unit ⊕ Nat
first_elt
[
1: Nat
1
,
2: Nat
2
] -- expect 1 (left)

#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.

def 
len: {α : Type} → List α → Nat
len
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
Nat: Type
Nat
| [] =>
0: Nat
0
|
Warning: unused variable `h` [linter.unusedVariables]
::
t: List α
t
=>
1: Nat
1
+
len: {α : Type} → List α → Nat
len
t: List α
t
0
@
len: {α : Type} → List α → Nat
len
Nat: Type
Nat
[]: List Nat
[]
-- expect 0
3
len: {α : Type} → List α → Nat
len
[
0: Nat
0
,
1: Nat
1
,
2: Nat
2
] -- expect 3
3
len: {α : Type} → List α → Nat
len
[
"I": String
"I"
,
"Love": String
"Love"
,
"Logic!": String
"Logic!"
] -- 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
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
) -- Test cases
true
reduce_and: List Bool → Bool
reduce_and
[
true: Bool
true
] -- expect true
false
reduce_and: List Bool → Bool
reduce_and
[
false: Bool
false
] -- expect false
true
reduce_and: List Bool → Bool
reduce_and
[
true: Bool
true
,
true: Bool
true
] -- expect true
false
reduce_and: List Bool → Bool
reduce_and
[
false: Bool
false
,
true: Bool
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
map_not
:
List: Type → Type
List
Bool: Type
Bool
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Bool
h
::
t: List Bool
t
=>
not: Bool → Bool
not
h: Bool
h
::
map_not: List Bool → List Bool
map_not
t: List Bool
t
-- hint: use :: to construct answer -- test cases
[]
map_not: List Bool → List Bool
map_not
[]: List Bool
[]
-- exect []
[false, true]
map_not: List Bool → List Bool
map_not
[
true: Bool
true
,
false: Bool
false
] -- 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
def 
countdown: Nat → List Nat
countdown
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
n': Nat
n'
+ 1 => (
n': Nat
n'
+
1: Nat
1
)::
countdown: Nat → List Nat
countdown
n': Nat
n'
-- test cases
[0]
countdown: Nat → List Nat
countdown
0: Nat
0
-- expect [0]
[5, 4, 3, 2, 1, 0]
countdown: Nat → List Nat
countdown
5: Nat
5
-- expect [5,4,3,2,1,0]

#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

def 
concat: {α : Type} → List α → List α → List α
concat
{
α: Type
α
:
Type: Type 1
Type
} :
List: Type → Type
List
α: Type
α
List: Type → Type
List
α: Type
α
List: Type → Type
List
α: Type
α
| [],
m: List α
m
=>
m: List α
m
|
h: α
h
::
t: List α
t
,
m: List α
m
=>
h: α
h
::
concat: {α : Type} → List α → List α → List α
concat
t: List α
t
m: List α
m
-- Test cases
[1, 2, 3]
concat: {α : Type} → List α → List α → List α
concat
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
]
[]: List Nat
[]
-- expect [1,2,3]
[1, 2, 3]
concat: {α : Type} → List α → List α → List α
concat
[]: List Nat
[]
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
] -- expect [1,2,3]
[1, 2, 3, 4]
concat: {α : Type} → List α → List α → List α
concat
[
1: Nat
1
,
2: Nat
2
] [
3: Nat
3
,
4: Nat
4
] -- expect [1,2,3,4]

#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
def 
pure': String → List String
pure'
:
String: Type
String
List: Type → Type
List
String: Type
String
|
s: String
s
=> [
s: String
s
]
["Hi"]
pure': String → List String
pure'
"Hi": String
"Hi"
-- expect ["Hi"]

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
#eval 
Error: unknown identifier 'pythag'
3 4 5: ?m.2
3 4 5
-- expect true #eval
Error: unknown identifier 'pythag'
6 7 8: ?m.21
6 7 8
-- expect false

#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
#eval 
Error: unknown identifier 'sum_cubes'
4: ?m.40
4
-- expect 100

#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.

def 
prod_ors_to_or_prods: {α β γ δ : Type} → (α ⊕ β) × (γ ⊕ δ) → α × γ ⊕ α × δ ⊕ β × γ ⊕ β × δ
prod_ors_to_or_prods
{
α: Type
α
β: Type
β
γ: Type
γ
δ: Type
δ
:
Type: Type 1
Type
} : (
α: Type
α
β: Type
β
) × (
γ: Type
γ
δ: Type
δ
)
α: Type
α
×
γ: Type
γ
α: Type
α
×
δ: Type
δ
β: Type
β
×
γ: Type
γ
β: Type
β
×
δ: Type
δ
| _ =>
_: α × γ ⊕ α × δ ⊕ β × γ ⊕ β × δ
_
Error: redundant alternative
Error: redundant alternative
Error: redundant alternative
-- 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 
Warning: declaration uses 'sorry'
Warning: declaration uses 'sorry'
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
1: Nat
1
=> [
1: Nat
1
] |
n': Nat
n'
+ 2 => have : (
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) < (
n': Nat
n'
+
2: Nat
2
) :=
sorry: shift_right (n' + 2) < n' + 2
sorry
nat_to_bin: Nat → List Nat
nat_to_bin
(
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) ++ [
right_bit: Nat → Nat
right_bit
(
n': Nat
n'
+
2: Nat
2
)] -- Left pad with zeros def
zero_pad: Nat → List Nat → List Nat
zero_pad
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
v: Nat
v
,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
(
v: Nat
v
- (
l: List Nat
l
.
length: {α : Type} → List α → Nat
length
))
l: List Nat
l
where
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
,
l: List Nat
l
=>
l: List Nat
l
|
v': Nat
v'
+1,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
v': Nat
v'
(
0: Nat
0
::
l: List Nat
l
) -- Make row of bits at index "row" padded out to "cols" wide def
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
: (
row: Nat
row
:
Nat: Type
Nat
) (
cols: Nat
cols
:
Nat: Type
Nat
)
List: Type → Type
List
Nat: Type
Nat
|
r: Nat
r
,
c: Nat
c
=>
zero_pad: Nat → List Nat → List Nat
zero_pad
c: Nat
c
(
nat_to_bin: Nat → List Nat
nat_to_bin
r: Nat
r
) -- Convert list of bits to list of bools def
bit_to_bool: Nat → Bool
bit_to_bool
:
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
=>
false: Bool
false
| _ =>
true: Bool
true
def
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
:
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Nat
h
::
t: List Nat
t
=> (
bit_to_bool: Nat → Bool
bit_to_bool
h: Nat
h
) :: (
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
t: List Nat
t
) -- Make row'th row of truth table with vars variables def
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
: (
row: Nat
row
:
Nat: Type
Nat
) (
vars: Nat
vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
r: Nat
r
,
v: Nat
v
=>
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
(
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
r: Nat
r
v: Nat
v
)

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
def 
Bread: Expr
Bread
:
Expr: Type
Expr
:= {
var.mk: Nat → var
var.mk
0: Nat
0
} def
Cheese: Expr
Cheese
:
Expr: Type
Expr
:= {
var.mk: Nat → var
var.mk
1: Nat
1
} def
Jam: Expr
Jam
:
Expr: Type
Expr
:= {
var.mk: Nat → var
var.mk
2: Nat
2
}
true
is_sat: Expr → Bool
is_sat
(
Bread: Expr
Bread
)
false
is_sat: Expr → Bool
is_sat
(
Bread: Expr
Bread
¬
Bread: Expr
Bread
)
false
is_valid: Expr → Bool
is_valid
(
Bread: Expr
Bread
¬
Bread: Expr
Bread
)
true
is_valid: Expr → Bool
is_valid
(
Bread: Expr
Bread
¬
Bread: Expr
Bread
)
true
is_unsat: Expr → Bool
is_unsat
(
Bread: Expr
Bread
¬
Bread: Expr
Bread
)

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 
Warning: declaration uses 'sorry'
Warning: declaration uses 'sorry'
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
1: Nat
1
=> [
1: Nat
1
] |
n': Nat
n'
+ 2 => have : (
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) < (
n': Nat
n'
+
2: Nat
2
) :=
sorry: shift_right (n' + 2) < n' + 2
sorry
nat_to_bin: Nat → List Nat
nat_to_bin
(
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) ++ [
right_bit: Nat → Nat
right_bit
(
n': Nat
n'
+
2: Nat
2
)] -- Left pad with zeros def
zero_pad: Nat → List Nat → List Nat
zero_pad
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
v: Nat
v
,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
(
v: Nat
v
- (
l: List Nat
l
.
length: {α : Type} → List α → Nat
length
))
l: List Nat
l
where
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
,
l: List Nat
l
=>
l: List Nat
l
|
v': Nat
v'
+1,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
v': Nat
v'
(
0: Nat
0
::
l: List Nat
l
) -- Make row of bits at index "row" padded out to "cols" wide def
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
: (
row: Nat
row
:
Nat: Type
Nat
) (
cols: Nat
cols
:
Nat: Type
Nat
)
List: Type → Type
List
Nat: Type
Nat
|
r: Nat
r
,
c: Nat
c
=>
zero_pad: Nat → List Nat → List Nat
zero_pad
c: Nat
c
(
nat_to_bin: Nat → List Nat
nat_to_bin
r: Nat
r
) -- Convert list of bits to list of bools def
bit_to_bool: Nat → Bool
bit_to_bool
:
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
=>
false: Bool
false
| _ =>
true: Bool
true
def
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
:
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Nat
h
::
t: List Nat
t
=> (
bit_to_bool: Nat → Bool
bit_to_bool
h: Nat
h
) :: (
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
t: List Nat
t
) -- Make row'th row of truth table with vars variables def
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
: (
row: Nat
row
:
Nat: Type
Nat
) (
vars: Nat
vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
r: Nat
r
,
v: Nat
v
=>
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
(
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
r: Nat
r
v: Nat
v
)

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

def 
and_elimination: {α β : Type} → α × β → α
and_elimination
{
α: Type
α
β: Type
β
:
Type: Type 1
Type
} :
α: Type
α
×
β: Type
β
α: Type
α
| (_, _) =>
Error: don't know how to synthesize placeholder context: α β : Type fst : α snd : β α

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} : (α  β) × γ)  γ)
| _ => 
Error: don't know how to synthesize placeholder context: α β γ : Type x : (α β) × γ) α γ

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)  α  β
| _, _ => 
Error: don't know how to synthesize placeholder context: α β : Type x✝¹ : α Empty x : α β

#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 := 
Error: don't know how to synthesize placeholder context: Sandwich : Sort u_1 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).

def 
bit_list_to_bool_list': List Nat → List Bool
bit_list_to_bool_list'
:
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Nat
h
::
t: List Nat
t
=> (
bit_to_bool: Nat → Bool
bit_to_bool
h: Nat
h
) :: (
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
t: List Nat
t
) -- expect [true, false, true, false, true]
[true, false, true, false, true]
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
[
1: Nat
1
,
0: Nat
0
,
3: Nat
3
,
0: Nat
0
,
1: Nat
1
]

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]
#eval 
Error: unknown identifier 'map'
bit_to_bool [1, 0, 1, 0, 1]: ?m.44293
bit_to_bool [1, 0, 1, 0, 1]

#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.

def 
X: Expr
X
:= {
var.mk: Nat → var
var.mk
0: Nat
0
}
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: unknown constant 'Expr.bot_exp'
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- 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.

likes iris fido : Prop
likes: dog → dog → Prop
likes
iris: dog
iris
fido: dog
fido
likes iris rover : Prop
likes: dog → dog → Prop
likes
iris: dog
iris
rover: dog
rover
likes iris iris : Prop
likes: dog → dog → Prop
likes
iris: dog
iris
iris: dog
iris

Moreover, we can state and prove propositions about dogs liking each other.

example: likes rover iris
example
:
likes: dog → dog → Prop
likes
rover: dog
rover
iris: dog
iris
:=
Error: don't know how to synthesize placeholder context: likes rover iris
-- 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 (
Error: don't know how to synthesize placeholder for argument 'h' context: square 2 4
) -- 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 : {α : Sort u_1} α α Prop
@
Eq: {α : Sort u_1} → α → α → Prop
Eq
-- @Eq : {α : Sort u} → α → α → Prop -- Sort u means any type -- The type of binary relations on the natural numbers
Eq : Nat Nat Prop
@
Eq: {α : Type} → α → α → Prop
Eq
Nat: Type
Nat
-- Eq : Nat → Nat → Prop -- The type of binary relation on strings
Eq : String String Prop
@
Eq: {α : Type} → α → α → Prop
Eq
String: Type
String
-- Eq : String → String → Prop

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.

1 = 2
@
Eq: {α : Type} → α → α → Prop
Eq
Nat: Type
Nat
1: Nat
1
2: Nat
2
-- The proposition that 1 = 2 -- We can omit the @ and let Lean infer the type argument
1 = 2
Eq: {α : Type} → α → α → Prop
Eq
1: Nat
1
2: Nat
2
-- a false equality proposition
0 = 0
Eq: {α : Type} → α → α → Prop
Eq
0: Nat
0
0: Nat
0
-- a true equality proposition
"Hi" = "Bob"
Eq: {α : Type} → α → α → Prop
Eq
"Hi": String
"Hi"
"Bob": String
"Bob"
-- a false equality of strings

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.

1 = "Hi" : Prop
Eq: {α : Type} → α → α → Prop
Eq
Error: failed to synthesize instance OfNat String 1
"Hi": String
"Hi"

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' 2 3 : Prop
Eq': {α : Type} → α → α → Prop
Eq'
2: Nat
2
3: Nat
3
-- Using our definition of Eq'
2 = 3 : Prop
Eq: {α : Type} → α → α → Prop
Eq
2: Nat
2
3: Nat
3
-- Lean supports infix = notation

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.

fun {α} {a} => Eq.refl a
@
rfl: ∀ {α : Sort u_1} {a : α}, a = a
rfl
-- fun {α} {a} => Eq.refl a
example: 1 = 1
example
:
1: Nat
1
=
1: Nat
1
:=
rfl: ∀ {α : Type} {a : α}, a = a
rfl
-- infers α = Nat, a = 1
example: 1 = 1
example
:
1: Nat
1
=
1: Nat
1
:=
Eq.refl: ∀ {α : Type} (a : α), a = a
Eq.refl
1: Nat
1
-- infers α = Nat, a = 1

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.subst : {α : Sort u_1} {motive : α Prop} {a b : α}, a = b motive a motive b
@
Eq.subst: ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive b
Eq.subst

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: Type
Person
:
Type: Type 1
Type
) (
Mary: Person
Mary
Maire: Person
Maire
:
Person: Type
Person
) (
Nice: Person → Prop
Nice
:
Person: Type
Person
Prop: Type
Prop
) (
h₁: Mary = Maire
h₁
:
Mary: Person
Mary
=
Maire: Person
Maire
) (
h₂: Nice Mary
h₂
:
Nice: Person → Prop
Nice
Mary: Person
Mary
)
@Eq.subst : {α : Sort u_1} {motive : α Prop} {a b : α}, a = b motive a motive b
@
Eq.subst: ∀ {α : Sort u_1} {motive : α → Prop} {a b : α}, a = b → motive a → motive b
Eq.subst
example: ∀ (Person : Type) (Mary Maire : Person) (Nice : Person → Prop), Mary = Maire → Nice Mary → Nice Maire
example
:
Nice: Person → Prop
Nice
Maire: Person
Maire
:=
Eq.subst: ∀ {α : Type} {motive : α → Prop} {a b : α}, a = b → motive a → motive b
Eq.subst
h₁: Mary = Maire
h₁
h₂: Nice Mary
h₂
end

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: ∀ {α : 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
:=

Goals accomplished! 🐙
-- use tactic mode to construct required term
α: Type
P: α Prop
a, b: α
h: a = b
pa: P a

P b
α: Type
P: α Prop
a, b: α
h: a = b
pa: P a

P a
α: Type
P: α Prop
a, b: α
h: a = b
pa: P a

P a
-- rewrite b to a in the goal

Goals accomplished! 🐙
-- we already have a proof of the goal; done

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 => 

Goals accomplished! 🐙
-- Put cursor here to see context
α: Type
a, b: α
h: a = b

b = a
α: Type
a, b: α
h: a = b

b = b

Goals 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.

-- Here

External 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.

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.