Zulip Chat Archive
Stream: general
Topic: Bespoke nats and algorithms
Kevin Buzzard (Oct 27 2023 at 14:30):
In the tutorial world of NNG4 I force people to prove 2+2=4 by unfolding numerals and rewriting add_zero
and add_succ
. I'm now adding an "algorithm world", an optional world where people who care more about the CS side of things can prove things like 20 + 20 = 40
.
I was surprised by several things in the experiment below.
1) You can prove 100+200=300 in MyNat
with rfl
quickly. Is this Lean just doing tens of thousands of manipulations quickly or is something cleverer going on?
2) But you can't prove 1000+2000=3000 with rfl
on MyNat
, although you can with Nat
. So there is special support for Nat
in rfl
?
3) simp
uses decide
and norm_num
uses simp
? Is that right?
4) But decide
on MyNat
is worse than rfl
?? (it can't do 100+200=300) Can I write a better decide
?
5) I am assuming that norm_num
is doing something clever. How much hard work is it to make norm_num
work on MyNat? Does this involve making binary nats? Can someone prove 1000+2000=3000
for MyNat
? (without the hack of bijecting it with Nat, doing it there, and then moving back?) Can I teach this in NNG4?
import Mathlib.Tactic
/-! Define bespoke naturals -/
inductive MyNat : Type
| zero : MyNat
| succ : MyNat → MyNat
namespace MyNat
/-! Get numerals working -/
def myNatFromNat (x : Nat) : MyNat :=
match x with
| Nat.zero => MyNat.zero
| Nat.succ b => MyNat.succ (myNatFromNat b)
instance ofNat {n : Nat} : OfNat MyNat n where
ofNat := myNatFromNat n
/-! Get addition working -/
def add : MyNat → MyNat → MyNat
| a, MyNat.zero => a
| a, MyNat.succ b => MyNat.succ (MyNat.add a b)
instance : Add MyNat where
add := MyNat.add
/-! `rfl` experiments -/
example : (10 : MyNat) + 20 = 30 := rfl -- OK
example : (100 : MyNat) + 200 = 300 := rfl -- how the heck does this work so quickly???
-- example : (1000 : MyNat) + 2000 = 3000 := rfl -- max recursion depth has been reached
example : (1000 : ℕ) + 2000 = 3000 := rfl -- ℕ not MyNat
/-! `simp` experiments -/
-- example : (10 : MyNat) + 20 = 30 := by simp -- simp made no progress
/-! Get decidability working -/
instance instDecidableEq : DecidableEq MyNat
| 0, 0 => isTrue <| rfl
| succ m, 0 => isFalse <| by rintro ⟨⟩
| 0, succ n => isFalse <| by rintro ⟨⟩
| succ m, succ n => match instDecidableEq m n with
| isTrue (h : m = n) => isTrue <| by rw [h]
| isFalse (h : m ≠ n) => isFalse <| by rintro ⟨h⟩; exact h rfl
/-! `decide` experiments -/
example : (10 : MyNat) + 20 = 30 := by decide -- OK
-- example : (100 : MyNat) + 200 = 300 := by decide -- max recursion depth
/-! `simp` again -/
example : (10 : MyNat) + 20 = 30 := by simp -- now works?!
-- example : (100 : MyNat) + 200 = 300 := by simp -- simp made no progress
/-! What about `norm_num`? -/
example : (10 : MyNat) + 20 = 30 := by norm_num -- presumably it tried `simp`?
-- example : (100 : MyNat) + 200 = 300 := by norm_num -- unsolved goals `100 + 200 = 300`
Eric Rodriguez (Oct 27 2023 at 14:33):
for 2), as far as I know, Nat
operations are reduced with GMP in the kernel, and therefore are much faster.
Arthur Adjedj (Oct 27 2023 at 15:07):
for 4), that's to be expected, rfl
will check for decidable equality between the two terms, looking at their shape after reduction. decide
on the other hand, is a function in your language, and will pattern-match on the two terms, and reduce iteratively, creating new terms and doing heavier computations. Note also that native_decide
handles bigger numbers with no issue, though it still causes a stack-overflow on numbers too big:
example : (5000 : MyNat) + 2000 = 7000 := by native_decide -- works
Kevin Buzzard (Oct 27 2023 at 15:10):
Oh thanks Arthur! So both algorithms reduce but then one of them starts cancelling and the other one just goes "yes they are the same".
Arthur Adjedj (Oct 27 2023 at 15:12):
rfl
reduces the two numbers and checks them syntactically. decide
has to reduce the comparison function you gave as a DecidableEq
instance, applied to your two arguments, doing heavier computations along the way, before checking whether the value is isTrue
of isFalse
.
Jannis Limperg (Oct 27 2023 at 15:34):
With a more efficient tail-recursive equality check, we can push decide
a bit farther. But it still can't do 7000; apparently the interpreter has some internal recursion limit even for tail-recursive functions.
inductive MyNat : Type
| zero : MyNat
| succ : MyNat → MyNat
namespace MyNat
def myNatFromNat (x : Nat) : MyNat :=
match x with
| Nat.zero => MyNat.zero
| Nat.succ b => MyNat.succ (myNatFromNat b)
instance ofNat {n : Nat} : OfNat MyNat n where
ofNat := myNatFromNat n
def add : MyNat → MyNat → MyNat
| a, MyNat.zero => a
| a, MyNat.succ b => MyNat.succ (MyNat.add a b)
instance : Add MyNat where
add := MyNat.add
def beq : MyNat → MyNat → Bool
| 0, 0 => true
| 0, succ _ => false
| succ _, 0 => false
| succ m, succ n => beq m n
theorem beq_eq : ∀ {n m}, beq n m → n = m
| 0, 0, _ => rfl
| succ _, succ _, h => congrArg _ (beq_eq h)
theorem beq_refl : ∀ n, beq n n
| 0 => rfl
| succ n => beq_refl n
theorem eq_beq (h : m = n) : beq m n := by
rw [h]; exact beq_refl _
instance instDecidableEq : DecidableEq MyNat := λ m n =>
if h : beq m n then
isTrue $ beq_eq h
else
isFalse λ contra => h (eq_beq contra)
example : (10 : MyNat) + 20 = 30 := by decide -- OK
example : (100 : MyNat) + 200 = 300 := by decide -- OK
example : (5000 : MyNat) + 2000 = 7000 := by decide -- max recursion depth
Reid Barton (Oct 27 2023 at 15:55):
For 1), isn't it only something like 600 operations?
Reid Barton (Oct 27 2023 at 15:58):
Computers are fast, e.g., this below runs in <1 second in the interpreter ghci, which is doing something vaguely analogous to kernel reduction:
myReplicate :: Integer -> a -> [a]
myReplicate 0 x = []
myReplicate n x = x : myReplicate (n-1) x
myEq :: Eq a => [a] -> [a] -> Bool
myEq [] [] = True
myEq (x:xs) (y:ys) = if x == y then myEq xs ys else False
myEq _ _ = False
myConcat :: [a] -> [a] -> [a]
myConcat [] ys = ys
myConcat (x:xs) ys = x : myConcat xs ys
main = print $
myEq
(myConcat (myReplicate 100000 ()) (myReplicate 200000 ()))
(myReplicate 300000 ())
Kevin Buzzard (Oct 27 2023 at 16:34):
So then why does 1000+2000 fail?
Mario Carneiro (Oct 27 2023 at 22:23):
@Reid Barton I think GHC will have bounded stack size for this implementation, but it is very unlikely that the kernel will have this property. I can check using lean4lean if you are interested, but my guess is that it's not strictly lazy evaluating, and a term of size 300000*<term size of some relatively large intermediate>
is actually produced at some point during the computation.
Improving kernel reduction has been something I've been looking into with Leo. There are a lot of low hanging fruits. In fact, if you (or anyone else) has examples of proofs that require a lot of kernel reduction, it would be helpful to send them to me so that I can demonstrate progress on them. I don't think the lean 4 test suite has much in this area.
Last updated: Dec 20 2023 at 11:08 UTC