Zulip Chat Archive

Stream: general

Topic: computable class


Kyle Miller (Mar 06 2022 at 21:16):

There had been some previous discussion about having a computable class that mirrors decidable, but for Type instead of Prop (two threads: 1 2). If it were possible, what this might let us do for mathlib is use mathematically convenient noncomputable definitions, then off to the side have decidable and computable instances for when we want to actually compute something. What I hope is (1) that this would free those who don't care about computability from having to think about adding the right decidability instances to their theorems (and even if you do care about computability, this is usually irrelevant information anyway), and (2) that this would alleviate diamond problems that come from including decidability instances in our class hierarchy.

The idea is to have a class

class computable {α : Type*} (x : α) :=
(compute : α)
(compute_eq : compute = x)

export computable(compute)

and then you would be able to write compute z when z is a noncomputable term that can be computed in some way.

When I've tried this in Lean 3, there's been a problem that computable instances need to be marked noncomputable, defeating the whole point of this class.

Lean 3 code

Oddly, #eval is happy to compute with this noncomputable instance anyway:

#eval compute (erased.of 2).out
-- 2

I tried implementing Computable today in Lean 4, and in my small tests it is able to tell that these instances are computable.

In this, there's the same erased example along with a noncomputable definition of inf for natural numbers that uses Nat.find to compute it.

This makes me wonder: is this a fundamental limitation of the noncomputability system in Lean 3, or is it a fixable bug?

Eric Wieser (Mar 06 2022 at 22:26):

I prototyped something like this a while ago, but with quite a different implementation

Eric Wieser (Mar 06 2022 at 22:28):

Although I guess actually that's not to similar to what you have, it just fits the same ambiguous "decidable but for Type" specification

Kyle Miller (Mar 06 2022 at 23:00):

If you think about decidable as being a term-level partial function to_bool : Prop -> bool that has the property that (to_bool p : Prop) = p, then it's clearer that this is what "decidable but for Type" sort of should be.

class decidable' (p : Prop) :=
(to_bool : bool)
(to_bool_eq : (to_bool : Prop) = p)

This does suggest that computable is too rigid in that it requires the computed value lie in the same type, so something like the following would be an alternative:

/-- Gives a type in which computations are more convenient.
Instances do not have to be computable. -/
class computable_type (α : Type*) (β : out_param $ Type*) :=
(cast : α  β)

class computable {α : Type*} {β : out_param $ Type*} [computable_type α β] (x : α) :=
(compute : β)
(compute_eq : compute = computable_type.cast x)

However, I'm not sure this is necessary since you could always compute (to_foo x) for a relevant to_foo function.

Kyle Miller (Mar 06 2022 at 23:46):

It looks like with enough of what Mario calls "laundering" through docs#erased and of applying the inline attribute, it actually is possible to get this to work in Lean 3. When I'd tried using erased like this before, I couldn't get if to compute, but compute (if 0 < m then Inf {n | n < m} else 0) seems to work fine now that the instances are inline.

Lean 3 code

With Lean 4, you don't need this additional complexity, and

instance [Decidable p] (x y : α) [Computable x] [Computable y] : Computable (if p then x else y) :=
if h : p then (by simp [h]; assumption) else (by simp [h]; assumption)

works fine.

Kyle Miller (Mar 07 2022 at 01:59):

Here's another experiment along these lines. In the spoiler block there are computable instances for getting coefficients of simple polynomial expressions (addition, multiplication, constant powers), and this lets you evaluate, for instance,

#eval compute $ ((1 + polynomial.X : [X])^10).coeff 3
-- 120

The implementation I gave for pow is pretty bad (I think it's exponentially slow in the exponent), but in any case it can be evaluated faster by adding in a specialized instance that relies on the binomial theorem:

@[inline]
instance binomial_compute (n k : ) :
  computable (erased.mk (((1 + polynomial.X) ^ n).coeff k : )) :=
n.choose k, by simp [polynomial.coeff_one_add_X_pow]⟩

#eval compute $ ((1 + polynomial.X : [X])^10).coeff 3
-- 120

Lean 3 code

Kyle Miller (Mar 07 2022 at 06:50):

Kyle Miller said:

This makes me wonder: is this a fundamental limitation of the noncomputability system in Lean 3, or is it a fixable bug?

If I've understood it correctly, this is fixable in Lean 3. The noncomputability checker wasn't skipping inductive type parameters in constructors (which I'm under the impression have no computational relevance), and if we make it skip those, the computable class would work just about the same between Lean 3 and Lean 4.

Would this break anything in theory?

Here's a PR: lean#693 (it passes all the tests on my machine, so it doesn't appear to have broken anything in that respect)

Kyle Miller (Mar 07 2022 at 06:56):

After the noncomputability checker, the compiler seems to do something similar and skip parameters and also additional computationally irrelevant arguments (these are handled by a separate mechanism in the noncomputability checker).

Kyle Miller (Mar 07 2022 at 07:03):

Another question: what's the actual role of noncomputable? If the noncomputability checker fails to say something is noncomputable, is the only effect that the code generator will eventually raise an error?

Johan Commelin (Mar 07 2022 at 08:32):

Kyle Miller said:

Here's another experiment along these lines. In the spoiler block there are computable instances for getting coefficients of simple polynomial expressions (addition, multiplication, constant powers), and this lets you evaluate, for instance,

#eval compute $ ((1 + polynomial.X : [X])^10).coeff 3
-- 120

That looks nice! I really like this. Of course something similar could be accomplished with norm_num. However, what I really like is the promise that this will get rid of all sorts of computability issues in definitions. We should be able to work with

class fintype (X : Type*) : Prop :=
cardinal.mk X < \omega

and just have some version of compute that takes care of computations in zmod 8 when needed.

Johan Commelin (Mar 07 2022 at 08:32):

Just to make sure, will

example : compute (((1 + polynomial.X : [X])^10).coeff 3) = 120 := rfl

also work?

Kyle Miller (Mar 07 2022 at 08:43):

That last one seems like the purview of norm_num, but there's nothing but the usual things stopping a rfl proof like that. That one's too big for my implementation, but something smaller is fine when it's using the binomial theorem instance:

example : compute (((1 + polynomial.X : [X])^3).coeff 2) = 3 := rfl

Kyle Miller (Mar 07 2022 at 08:48):

Johan Commelin said:

and just have some version of compute that takes care of computations in zmod 8 when needed.

I imagine finset could stay as-is, just with noncomputable operations in the API that become computable when you compute with the right decidability instances. Then, if we had your proposed fintype class, fintype.univ could noncomputably give a finset that becomes computable when computed. (I think that's behind fin_cases, which I think you're referring to with zmod 8.)

Gabriel Ebner (Mar 07 2022 at 10:16):

This is an interesting solution to the computing with polynomials issue.

Gabriel Ebner (Mar 07 2022 at 10:18):

I know you're keen on getting rid of the laundering, but the class Computable (x : Erased α) where compute : α ... variant has one big advantage, namely that you can write the blanket instance instance [Computable (erase a)] where compute := a ....

Gabriel Ebner (Mar 07 2022 at 10:19):

Without this blanket instance, I imagine it will be awkward to compute open terms like this:

def foo (n : ) := compute $ ((1 + polynomial.X : [X])^(2*n)).coeff n

Gabriel Ebner (Mar 07 2022 at 10:21):

On second thought, I'm not sure the blanket instance helps at all.

Gabriel Ebner (Mar 07 2022 at 10:24):

BTW, you probably want to generalize your ite-compute instance so that it gets rid of classical.prop_decidable:

instance [Decidable p] {inst : Decidable p} (x y : α) [Computable x] [Computable y] :
    Computable (@ite _ p inst x y)

Kyle Miller (Mar 07 2022 at 10:32):

Gabriel Ebner said:

Without this blanket instance, I imagine it will be awkward to compute open terms like this:

def foo (n : ) := compute $ ((1 + polynomial.X : [X])^(2*n)).coeff n

At least with how it's set up, this one is fine since the exponent is presumed to be computable, and it's up to the caller of foo to compute the natural number if necessary.

#eval foo 10
-- 184756

Last updated: Dec 20 2023 at 11:08 UTC