Documentation

PnP2023.Lec_01_18.NatSub

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)
def Nat.sub! (m : ) (n : ) :

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.

    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.

      A natural number obtained by panicking.

      Instances For

        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.

        
        

        The default value in .

        Instances For
          #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
          

          The default value in ℕ × ℕ.

          Instances For

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

            The default value in ℕ × String × (String → ℕ).

            Instances For
              def default₃ :
              × (String) × (EmptyEmpty)

              The default value in ℕ × (String → ℕ) × (Empty → Empty).

              Instances For

                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.

                inductive MyEmpty :

                  An empty type.

                  Instances For
                    instance instInhabitedForAll_3 (α : Type) :
                    Inhabited (αα)

                    An instance of Inhabited corresponding to the identity function from any type to itself.

                    def default₄ :
                    × (String) × (MyEmptyMyEmpty)

                    The default value in ℕ × (String → ℕ) × (ℕ → MyEmpty → MyEmpty).

                    Instances For

                      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)
                      
                      def default₅ :
                      × (String) × (EmptyEmpty)

                      The default value in ℕ × String (String → ℕ) × (Empty × Empty) in the presence of the identity default instance.

                      Instances For
                        def default₆ :
                        × () × (MyEmptyMyEmpty)

                        The default value in ℕ × String (ℕ → ℕ) × (MyEmpty × MyEmpty) in the presence of the identity default instance.

                        Instances For

                          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.

                          def Nat.sub? :
                          Option

                          Option valued subtraction of natural numbers

                          Equations
                          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
                            
                            def Nat.doubleSub? (m : ) (n : ) :

                            Optionally return (m - n) * 2 if 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
                              
                              def Nat.tripleSub? (m : ) (n : ) :

                              Optionally return (m - n) * 3 if m ≥ n.

                              Instances For
                                def Nat.sub_sub? (a : ) (b : ) (c : ) :

                                Optionally return a - b - c if this is non-negative.

                                Instances For

                                  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₂