Zulip Chat Archive

Stream: new members

Topic: nonconstructive proofs


Lucas Teixeira (Aug 30 2021 at 16:44):

Is it possible to write non constructive proofs in LEAN??

Mario Carneiro (Aug 30 2021 at 16:45):

yes

Eric Wieser (Aug 30 2021 at 16:45):

Yes, using docs#classical.choice

Mario Carneiro (Aug 30 2021 at 16:45):

Most mathematics in mathlib makes no attempt to be constructive

Kevin Buzzard (Aug 30 2021 at 18:36):

Mathematicians like playing in easy mode. They can get much further on in the quest that way.

Will Sawin (Aug 30 2021 at 18:43):

What fraction of the results in mathlib, if they were proved constructively, would produce a useful algorithm?

Kevin Buzzard (Aug 30 2021 at 18:50):

if it were useful it would be in core Lean, right?

Kevin Buzzard (Aug 30 2021 at 18:52):

The stuff where you can clearly do it constructively is in my experience mostly done constructively. Sometimes this makes it harder to use (or perhaps harder for beginners to use).

Johan Commelin (Aug 30 2021 at 18:52):

@Will Sawin Sometimes we use constructive results. But running time blows up quickly.

Johan Commelin (Aug 30 2021 at 18:53):

I think that Chris used it a couple of times when doing computations modulo small nn, when he formalized quadratic reciprocity and sum of 4 squares

Johan Commelin (Aug 30 2021 at 18:54):

Because for small n you can just do computations by brute force, and if stuff is done constructively, the algorithm for the brute force is handed to you by Lean.

Johan Commelin (Aug 30 2021 at 18:54):

But in most cases the definitions that are good for elegant proofs are bad for fast computations.

Yakov Pechersky (Aug 30 2021 at 18:57):

We do computed proofs often via norm_num =)

Yakov Pechersky (Aug 30 2021 at 18:57):

But in terms of data structures, or enumerating over inductive data types, yes, a lot of algorithms are very slow.

Kyle Miller (Aug 30 2021 at 19:09):

Maybe another illustrative example is finite sets. To be as generic as possible, they're defined to be duplicate-free lists up to reordering. The only requirement they have is that there be an algorithm for testing equality. The only possible algorithm for testing set membership in this situation takes O(n)O(n) time, since every element of that list must be compared for equality.

If finite sets could require that there be a computable total order, then that could be dropped down to O(lnn)O(\ln n) using a tree representation for the finite set. Practical finite sets use hashing, which have O(1)O(1) set membership (for small enough finite sets... so not completely accurate).

So, while finite sets are constructive, and most operations on them are constructive (in that they are backed by algorithms), they're not exactly the algorithms you'd want to use in practice.

There doesn't seem to be a good way to have multiple algorithms and data structures with different characteristics for the same mathematical object, short of redefining the object and proving all its properties multiple times...

It would be cool if there were a way to have the pure math version of an object then provide computational definitions along with hints for how Lean should evaluate something. Maybe even if there were a way to write a typeclass that could give computational meaning to noncomputable definitions, which I don't think is possible with the way noncomputable works.

Kevin Buzzard (Aug 30 2021 at 19:11):

Isn't this Lean 4's @implementedBy?

Kevin Buzzard (Aug 30 2021 at 19:18):

Johan Commelin said:

But in most cases the definitions that are good for elegant proofs are bad for fast computations.

A fabulous case in point being the natural numbers. Ever tried to prove multiplication is associative by binary induction (i.e. induction on length of binary representation of number)?

Kyle Miller (Aug 30 2021 at 19:21):

@Kevin Buzzard That's a bit different -- as far as I understand, it gives just one implementation for something. It would be nice if you could have different implementations depending on which typeclasses are available, since additional structure can allow for faster algorithms. You sort have to go for the lowest common denominator without this.

Yakov Pechersky (Aug 30 2021 at 19:21):

You could have a version of the function that has more constraints (your typeclass constraints), and that one is implementedBy

Kevin Buzzard (Aug 30 2021 at 19:22):

Right -- for example matrix multiplication is done very differently for sparse matrices compared with the generic case.

Yakov Pechersky (Aug 30 2021 at 19:22):

The sparse matrix case is hard, because how do you infer that you have a sparse matrix?

Kevin Buzzard (Aug 30 2021 at 19:23):

Because the user tells you? This is for people wanting to do computations, right? Don't ask me :-)

Kevin Buzzard (Aug 30 2021 at 19:24):

That's how it works in computer algebra packages, you just use the matrix multiplication function but give it some added switch to say "please use this algo at this point"

Kevin Buzzard (Aug 30 2021 at 19:27):

example using Sage

Kyle Miller (Sep 09 2021 at 19:00):

One way it seems you can work with noncomputable definitions and then provide (multiple) ways to compute them is through a "classicalized" version of a type. Every noncomputable value can be represented as a singleton set:

structure sval (α : Type u) :=
(s : set α)
(singleton :  x, s = {x})

prefix `!`:100 := sval

def sval.incl {α : Type u} (x : α) : !α := ⟨{x}, x, rfl

For example, the infimum of a set of natural numbers in noncomputable, but we can erase this keyword using this type:

def Inf' (s : set ) : ! := ⟨{Inf s}, _, rfl

The next ingredient is a typeclass similar to decidable that provides a computation (when one is available):

class computable {α : Type*} (v : !α) :=
(compute : α)
(compute_spec : compute  v.s)

def sval.compute {α : Type*} (v : !α) [computable v] : α := computable.compute v

For example, the infimum is computable if the set has decidable membership and if nonemptiness is decidable:

instance nat.Inf'_computable (s : set ) [decidable_pred ( s)] [h : decidable s.nonempty] : computable (Inf' s) :=
... something involving nat.find ...

It can also be computable if you know exactly what the set is:

instance nat.Inf'_interval (a : ) : computable (Inf' (set.Ici a)) := a, by simp [Inf']⟩

#eval (Inf' (set.Ici 100)).compute
-- 100

Mario Carneiro (Sep 09 2021 at 19:03):

By the way, sval is also known as docs#erased

Kyle Miller (Sep 09 2021 at 19:03):

It's not the most convenient to work with, but there are a few interesting Lean-computable equivalences.

  • set (!α) ≃ set α
  • !set α ≃ set α
  • !(Π (x : α), β x) ≃ (Π (x : α), !β x) and !(α → β) ≃ (α → !β)

It's also a monad, so at least that's something.

Kyle Miller (Sep 09 2021 at 19:04):

@Mario Carneiro Thanks, I knew there had to be something like this already.

Kyle Miller (Sep 09 2021 at 19:05):

Here's a gist https://gist.github.com/kmill/a67dae0e6cdf8ff8897d37a0563f8d4b with the experiment

Kyle Miller (Sep 09 2021 at 19:08):

(I'd wanted to try making some computable classes that would demonstrate you could use ordset as an intermediate representation when taking some unions of finsets, but I didn't get to it, if it's even possible.)

Kyle Miller (Sep 09 2021 at 19:09):

That's nice that erased erases its data -- I'd worried about whether sval would develop enormous symbolic representations.

Kyle Miller (Sep 09 2021 at 19:16):

One more example of computable, for noncomputable finite sets (showing only some illustrative definitions and lemmas):

abbreviation finset' (α : Type*) := !finset α

instance : has_union (finset' α) :=
λ s t, ⟨{u | by { classical, exact u = s.val  t.val }}, _, rfl⟩⟩

lemma mem_union {s t : finset' α} (x : α) : x  s  t  x  s  x  t :=
by { change x  sval.incl _  _,  simp }

instance (s t : finset' α) [computable s] [computable t] [decidable_eq α] : computable (s  t) := ...

abbreviation a : finset'  := sval.incl {1,2}
abbreviation b : finset'  := sval.incl {2,3,4}

#eval (a  b).compute
-- {1, 2, 3, 4}

(This needs a and b to be reducible since it's relying on another instance that says anything created with sval.incl is computable.)

Reid Barton (Sep 09 2021 at 19:21):

This is pretty neat but it's worth considering the trade-off compared to something in the style of norm_num (or even simp). As an extreme example, in order to prove that a given number is composite, norm_num could invoke an external program to factor the number.

Mario Carneiro (Sep 09 2021 at 19:23):

Also, norm_num is allowed to fail sometimes, while decidability instances generally can't, unless you do the bulk of the computation in the typeclass inference search itself

Reid Barton (Sep 09 2021 at 19:23):

The biggest advantage of something like computable is that you get a meta-level proof that there is an algorithm to compute the thing... but if you only really care about actually computing it then this isn't really necessary.

Reid Barton (Sep 09 2021 at 19:24):

(this is basically the same as Mario's point that norm_num is allowed to fail)

Kyle Miller (Sep 09 2021 at 19:25):

Yeah, I don't think this is very useful for compile-time (the likes of norm_num and simp). It seems like it could be useful for when you want to compile programs and want to be able to automatically select good-ish algorithms.

Kyle Miller (Sep 09 2021 at 19:25):

while also being able to use the standard mathematical definitions, which come with a whole library of lemmas

Reid Barton (Sep 09 2021 at 19:26):

It's definitely an improvement over trying to make your algorithm be the definition, I think.

Mario Carneiro (Sep 09 2021 at 19:26):

For example, for interval arithmetic computations on real numbers it is convenient to make the calculation blow up if you divide by zero, but that means that the space of successfully evaluating terms is very complicated and depends on the output of previous computations

Kyle Miller (Sep 09 2021 at 19:34):

Reid Barton said:

The biggest advantage of something like computable is that you get a meta-level proof that there is an algorithm to compute the thing... but if you only really care about actually computing it then this isn't really necessary.

This all applies to decidable too, right?

One reason I was looking into this was to see whether decidable could feasibly be removed from all the mathematical definitions, and whether it could be relegated to computable instances.

Mario Carneiro (Sep 09 2021 at 19:36):

Yes, decidable has roughly the same characteristics as computable here

Mario Carneiro (Sep 09 2021 at 19:37):

decidable can't be removed from definitions with computational content, whether it is spelled decidable or computable

Mario Carneiro (Sep 09 2021 at 19:38):

Note that your finset' definition does not make finset obsolete, because it depends on finset

Mario Carneiro (Sep 09 2021 at 19:38):

the theorems about finset would continue to have all the same decidability assumptions

Kyle Miller (Sep 09 2021 at 19:44):

I designed computable to be the Type-level version of decidable -- I meant the question rhetorically.

And sure, I used finset to implement finset', but I didn't have to. The point of the example is that finset can be used as a computational backend to the classical notion of a finite set. finset certainly needs its decidability assumptions.

Mario Carneiro (Sep 09 2021 at 19:46):

It's not clear that we gain that much by having finset' though, compared to open_locale classical + finset

Mario Carneiro (Sep 09 2021 at 19:47):

It's certainly possible to have a version of finset with no computational content; finite does basically that

Mario Carneiro (Sep 09 2021 at 19:48):

explicitly erasing computational content isn't usually that productive, since it just removes some noncomputable markings

Kyle Miller (Sep 09 2021 at 19:48):

Based on your saying "whether it is spelled decidable or computable" I'm thinking what I said isn't clear:
Kyle Miller said:

One reason I was looking into this was to see whether decidable could feasibly be removed from all the mathematical definitions, and whether it could be relegated to computable instances.

This is what I'm referring to specifically:

instance (s t : finset' α) [computable s] [computable t] [decidable_eq α] : computable (s  t) := ...

It's moving any trace of decidable from definitions and lemmas about mathematical definitions to descriptions of how you might compute them.

Kyle Miller (Sep 09 2021 at 19:49):

This is also a much more precise way to give computational meaning to expressions, since you can make use of any additional typeclasses the types might satisfy.

Mario Carneiro (Sep 09 2021 at 19:51):

It's still not good enough for e.g. selecting sparse matrix vs dense matrix representation, but sure, this works alright for decidable

Reid Barton (Sep 09 2021 at 19:51):

so for example, get rid of ite with its decidable argument, and instead write a computable instance for it which carries the decidable hypothesis

Kyle Miller (Sep 09 2021 at 19:52):

/-- ite, but without decidability -/
def ite' {α : Type*} (c : Prop) (x y : !α) : !α :=
⟨{v | (c  v  x.s)  (¬c  v  y.s)},
begin
  by_cases c,
  { use x.val, simp [h], },
  { use y.val, simp [h], },
end

/-- When the proposition is decidable, compute the ite' with ite. -/
instance ite'.compute {α : Type*} (c : Prop) [decidable c] (x y : !α) [computable x] [computable y] : computable (ite' c x y) :=
ite c x.compute y.compute,
begin
  unfold ite',
  dsimp,
  simp {contextual:=tt},
end

Kyle Miller (Sep 09 2021 at 19:53):

This is using the ! type, which is annoying -- I wish it were more seamless -- but in principle it seems to work.

Reid Barton (Sep 09 2021 at 19:53):

I was thinking of a noncomputable version

Mario Carneiro (Sep 09 2021 at 19:53):

You can write ite' more simply as launder (by classical; exact ite c x y) where launder is an identity function on that makes any term computable

Mario Carneiro (Sep 09 2021 at 19:56):

Proving these things can be annoying for recursive functions though

Mario Carneiro (Sep 09 2021 at 19:58):

I would rather come at this from the other direction: introduce ite' and use it only when the decidability argument gets in the way

Mario Carneiro (Sep 09 2021 at 19:59):

(and it would be defined simply as by classical; exact ite c x y, noncomputable notwithstanding)

Kyle Miller (Sep 09 2021 at 20:00):

It would be nice if there were a way to coerce noncomputable terms to . It's not clear to me how you'd do it without the coercion itself being noncomputable.

Kyle Miller (Sep 09 2021 at 20:01):

I didn't fully understand what launder was about -- did you mean it would be this coercion?

Mario Carneiro (Sep 09 2021 at 20:09):

import data.erased

axiom magic : erased 

noncomputable def magic' : erased  := magic -- if we use it directly, it is noncomputable

def magic'' : erased  := magic.1, magic.2 -- laundering it makes it computable

def launder {α} (s : erased α) : erased α := s.1, s.2

-- Using launder directly doesn't work, because lean doesn't unfold it
noncomputable def magic''' : erased  := launder magic

notation `launder` s := s.1, s.2

-- @[inline] also doesn't work (and lean 3 doesn't have macroInline), but a notation does
def magic'''' : erased  := launder magic

Mario Carneiro (Sep 09 2021 at 20:16):

Oh, looks like abbreviation works, wasn't expecting that

Jake Levinson (Mar 03 2022 at 07:11):

Hi @Mario Carneiro and @Kyle Miller , I found this thread by searching for "nonconstructive ite". Did anything like this end up getting added to mathlib? (I hope it's ok to respond to an old thread)

Mario Carneiro said:

It's not clear that we gain that much by having finset' though, compared to open_locale classical + finset

Quick comment -- I ran into problems a few weeks ago from trying to use open_locale classical + finset, problems coming from decidability diamonds. Basically the solution I was told was to explicitly provide decidability instances rather than using open_locale classical.

I don't really understand decidability, but (now that I'm trying to use it appropriately) I just ran into a similar kind of problem with two superficially identical ite expressions with different decidability instances. It would definitely be very nice to have a way to do this that doesn't involve any decidability at all.

Kevin Buzzard (Mar 03 2022 at 08:33):

Yes, open_locale classical was an attempt to fix some decidability issues but now there are better ways (the rule of thumb is that if the statement of a declaration needs one then add it as an assumption, and if a proof needs one then use the classical tactic). This is what you do in the other thread but you still seem to get into problems :-/

Jake Levinson (Mar 03 2022 at 08:38):

I do appreciate your help :smile:


Last updated: Dec 20 2023 at 11:08 UTC