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