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 inzmod 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 compute
d. (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