Subtraction: example of intertwined proofs and definitions. #
The difference of natural numbers is not a natural number. We see three ways of overcoming this problem, which illustrate various important concepts in Lean.
The first two are in this module while the third is in a sequel. The first two are in fact available in functional programming langauges such as scala
and Haskell
.
To begin with, we see how Lean deals with subtraction on ℕ
.
#eval 4 - 3 -- 1
#eval 3 - 4 -- 0
The first example is as expected, but the second may be surprising. According to the documentation of Lean 4, Nat.sub
is:
> (Truncated) subtraction of natural numbers. Because natural numbers are not closed under subtraction, we define m - n
to be 0 when n < m
.
Subtraction with panic #
Our first remedy is to define subtraction as Lean does but with an error message when the result is incorrect.
def Nat.sub! (m n: Nat) :=
match m, n with
| m, 0 => m
| m + 1, n + 1 => Nat.sub! m n
| 0, _ + 1 => panic! "cannot subtract a larger number from a smaller one"
#eval Nat.sub! 4 3 -- 1
#eval Nat.sub! 3 4 -- 0 (but with an error message)
Subtraction of natural numbers; panics when difference is negative.
Equations
Instances For
A brief digression: Lean 4 lets us easily introduce new notation for our variant of subtraction.
infix:64 "-!" => Nat.sub!
infix notation for subtraction with panic.
Equations
- «term_-!_» = Lean.ParserDescr.trailingNode `term_-!_ 64 65 (Lean.ParserDescr.binary `andthen (Lean.ParserDescr.symbol "-!") (Lean.ParserDescr.cat `term 65))
Instances For
More on panicking #
The intriguing fact about the illegal subtraction is that, while it gave an error, it still had a value. Indeed, we see more of the underlying phenomenon in the following examples.
def panicNat : ℕ := panic! "I like to panic"
#check panicNat -- ℕ
-- #eval panicNat -- 0 (with error)
Note that panicNat
had a type, and the computation of the type did not give an error. Hence for logical consistency, the value of panicNat
must be a term of type ℕ
.
Indeed, if we try to make an analogous definition for Empty
we get an error.
def badPanic : Empty :=
panic! "sometimes we are not even allowed to panic"
gives the error message:
failed to synthesize instance
Inhabited Empty
Indeed the empty type has no inhabitants so allowing a definition of badPanic
would be a contradiction.
Default values and typeclasses #
The value returned when panicing is the default
value of the type. Not every type has a default value. For example, the Empty
type has no default value.
Default values can be synthesized from other default values by so called typeclass inference. First we see some examples of default values.
#eval defaultNat -- 0
As we have seen earlier, the default value of ℕ
is 0
.
As we saw in the case of panic, the default value of Empty
is not defined. The following gives an error message.
def defaultEmpty : Empty := default
A more interesting example is the default value of a product type.
#eval default₁ -- (0, 0)
This is inferred from the default values of the components.
Typeclasses #
We sketch the basic ideas of typeclasses and how they are used here. The default
value of a type α
is based on Inhabited α
.
Some more examples of typeclass inference.
#reduce default₂ -- (Nat.zero, "", fun x => Nat.zero)
#reduce default₃ -- (Nat.zero, fun x => Nat.zero, fun a => False.rec (fun x => Empty) (_ : False))
In the first example, we see that default functions are inferred if the codomains are inhabited, with a constant function used as a default.
Lean has inferred a default function from Empty
to Empty
by using the default function from Empty
to False
. To illustrate introducing new defaults we introduce a new type MyEmpty
which is also an empty type.
An instance of Inhabited
corresponding to the identity function from any type to itself.
Equations
- instInhabitedForAll_3 α = { default := id }
We see this picked up in the following construction. Note that defining a default for MyEmpty
gives an error.
#reduce default₄ -- (Nat.zero, fun x => Nat.zero, fun x a => a)
The following example shows the effect of priorities of instances.
#reduce default₆ -- (Nat.zero, fun x => Nat.zero, fun x a => a)
Observe that the second component is the constant function fun x => Nat.zero
and not the identity function id
.
Second rectification: Option
#
The second choice is to essentially return values only when they are valid, by wrapping them in an Option
.
Equations
- «term_-?_» = Lean.ParserDescr.trailingNode `term_-?_ 64 65 (Lean.ParserDescr.binary `andthen (Lean.ParserDescr.symbol "-?") (Lean.ParserDescr.cat `term 65))
Instances For
Some examples of subtraction returning option types.
#eval 4 -? 3 -- some 1
#eval 3 -? 4 -- none
If we return option types we need to be able to handle them. We illustrate this by defining a function that returns the double of the difference if it is defined.
def Nat.doubleSub? (m n : ℕ) : Option ℕ :=
(m -? n).map (· * 2)
#eval Nat.doubleSub? 5 3 -- some 4
#eval Nat.doubleSub? 5 32 -- none
Optionally return (m - n) * 2
if m ≥ n
.
Equations
- Nat.doubleSub? m n = Option.map (fun (x : ℕ) => x * 2) (m-?n)
Instances For
A convenient way to handle option types is to use the do
notation.
def Nat.tripleSub? (m n : ℕ) : Option ℕ :=
do
let d ← m -? n
return d * 3
#eval Nat.tripleSub? 5 3 -- some 6
The do
notation is even more convenient when we compose option valued functions.
def Nat.sub_sub? (a b c : ℕ) : Option ℕ :=
do
let d₁ ← a -? b
let d₂ ← d₁ -? c
return d₂