Zulip Chat Archive
Stream: maths
Topic: Countability of inductive type
Violeta Hernández (Oct 31 2024 at 01:46):
I'm working with the following definition:
inductive PreCantor : Type
/-- The ordinal `0` -/
| zero : PreCantor
/-- The ordinal `oadd e n a = ω ^ e * n + a` -/
| oadd : PreCantor → ℕ+ → PreCantor → PreCantor
It seems clear to me that this type must be countable. Specifically, one can subdivide the type into "layers" based on the number of applications of the constructor oadd
. The cardinality c (n + 1)
of layer n + 1
is equal to c n * ℵ₀ * c n
, which through a simple induction implies c n = ℵ₀
and thus that the entire type is countable.
It seems like this argument should follow in more generality, however. Specifically, any inductive type α
whose constructors β₁ → β₂ → ... → α
refer only to α
and other countable types should be countable. Is there some sort of machinery already in Mathlib formalizing these sorts of arguments?
Violeta Hernández (Oct 31 2024 at 01:46):
I'm reminded of the docs#WType API, but I don't understand WType
enough to know whether this is an instance of it or not
Violeta Hernández (Oct 31 2024 at 02:12):
I think this can be injected into a WType
where the possible labels are in Option ℕ
, with 0
representing zero
, other natural numbers representing themselves, and none
representing the oadd
constructor
Kyle Miller (Oct 31 2024 at 02:32):
Ideally we would have deriving Countable
for this.
An idea I've played around with before is to define a type simpler than WType
specifically for doing Countable
arguments. Basically, once you have that S
is Countable
in the following, it becomes fairly easy to implement the deriving Countable
handler.
inductive PreCantor : Type
/-- The ordinal `0` -/
| zero : PreCantor
/-- The ordinal `oadd e n a = ω ^ e * n + a` -/
| oadd : PreCantor → ℕ+ → PreCantor → PreCantor
inductive S : Type where
| nat (n : ℕ)
| cons (a b : S)
instance : Coe ℕ S := ⟨S.nat⟩
instance (n : Nat) : OfNat S n := ⟨n⟩
def PNat.toS (n : ℕ+) : S := (n : ℕ)
theorem PNat.toS_injective : Function.Injective PNat.toS := by
intro; simp (config := {contextual := true}) [toS]
def PreCantor.toS : PreCantor → S
| .zero => .cons 0 0
| .oadd x n y => .cons 1 (.cons x.toS (.cons n.toS y.toS))
theorem PreCantor.toS_injective : Function.Injective PreCantor.toS := by
intro x
induction x with
| zero => rintro (_|_) <;> simp [toS]
| oadd x n y ihx ihy =>
rintro (_|_)
· simp [toS]
simp [toS]
intro h1 h2 h3
cases ihx h1
cases ihy h3
cases PNat.toS_injective h2
simp
Kyle Miller (Oct 31 2024 at 02:32):
You don't need a toS
function for each type that appears inside the inductive definition, like I did for PNat here. Instead, you use choice to extract an injective function to Nat from the Countable proof.
Kyle Miller (Oct 31 2024 at 02:34):
Proving S
is countable should be easy. You can construct a function to Nat directly, using that Nat pairing function in mathlib
Kyle Miller (Oct 31 2024 at 02:39):
I'd be happy to help make this Countable deriving handler :-) If you write the Countable
proof for S
, I'll look into the next steps.
Violeta Hernández (Oct 31 2024 at 02:43):
Kyle Miller said:
I'd be happy to help make this Countable deriving handler :-) If you write the
Countable
proof forS
, I'll look into the next steps.
Easy!
inductive S : Type where
| nat (n : ℕ)
| cons (a b : S)
def S.toNat : S → ℕ
| nat n => Nat.pair 0 n
| cons a b => Nat.pair (a.toNat + 1) b.toNat
theorem S.toNat_injective : Function.Injective S.toNat
| nat m, nat n
| cons a b, nat n
| nat m, cons a b => by simp [S.toNat]
| cons a b, cons c d => by
simp_rw [toNat, Nat.pair_eq_pair, add_left_inj, cons.injEq]
exact fun ⟨h₁, h₂⟩ ↦ ⟨S.toNat_injective h₁, S.toNat_injective h₂⟩
instance : Countable S :=
S.toNat_injective.countable
Violeta Hernández (Oct 31 2024 at 02:44):
You idea to use Nat.pair
was really clever, my own idea was trying to inject S
into a WType
Chris Hughes (Oct 31 2024 at 21:31):
Have you looked at Data.W.Basic
? There's an encodable
instance there. You could prove it's isomorphic to the corresponding WType
and use the instance.
Violeta Hernández (Oct 31 2024 at 21:47):
Violeta Hernández said:
I think this can be injected into a
WType
where the possible labels are inOption ℕ
, with0
representingzero
, other natural numbers representing themselves, andnone
representing theoadd
constructor
I did think about this, but Kyle's approach seems simpler
Eric Wieser (Nov 01 2024 at 03:00):
Is Kyle's approach above sensible for implementing docs#Encodable too?
Violeta Hernández (Nov 01 2024 at 03:02):
It should be, I think. The explicit encoding would look something like x.toS.toNat
, and building the inverse should be easy as long as we can show S itself is encodable.
Kyle Miller (Nov 02 2024 at 18:46):
@Violeta Hernández, it is done: #18557
Kyle Miller (Nov 02 2024 at 18:47):
It turned out to be a easier to not go through any auxiliary types. I spent time looking at your S.toNat_injective
and figured out how to generalize it for automation. If you want to see the details, there's an example expansion in a module docstring in the DeriveCountable file.
Kyle Miller (Nov 02 2024 at 20:39):
I tried working out the theory of how to go about making an Encodable deriving handler. It was really painful (and I failed) trying to use Nat.pair
directly, and the intermediate S
type was helpful.
It seems like it's a promising approach though
import Mathlib
inductive S : Type where
| nat (n : ℕ)
| cons (a b : S)
instance : Coe ℕ S := ⟨S.nat⟩
instance (n : Nat) : OfNat S n := ⟨n⟩
def S.encode : S → ℕ
| nat n => Nat.pair 0 n
| cons a b => Nat.pair (a.encode + 1) b.encode
lemma Nat.unpair_lt_2 {n : ℕ} (n1 : 1 ≤ (Nat.unpair n).1) : (Nat.unpair n).2 < n := by
obtain ⟨⟨a, b⟩, rfl⟩ := Nat.pairEquiv.surjective n
simp only [pairEquiv_apply, Function.uncurry_apply_pair, unpair_pair] at *
simp [pair]
split <;> nlinarith
def S.decode (n : ℕ) : S :=
let p := Nat.unpair n
if h : p.1 = 0 then
S.nat p.2
else
have : p.1 ≤ n := Nat.unpair_left_le n
have := Nat.unpair_lt (by omega : 1 ≤ n)
have := Nat.unpair_lt_2 (by omega : 1 ≤ p.1)
S.cons (S.decode (p.1 - 1)) (S.decode p.2)
theorem S.decode_encode (s : S) : S.decode (S.encode s) = s := by
unfold S.decode
dsimp only
cases s with
| nat n =>
unfold S.encode
simp
| cons a b =>
unfold S.encode
simp [S.decode_encode a, S.decode_encode b]
instance : Encodable S where
encode := S.encode
decode := fun s => some (S.decode s)
encodek := by simp [S.decode_encode]
-- Example:
inductive T (α : Type) where
| a (x : α) (y : Bool) (z : T α)
| b
def T.encode {α : Type} [Encodable α] : T α → S
| a x y z =>
.cons 0 (.cons (Encodable.encode x) (.cons (Encodable.encode y) (T.encode z)))
| b => .cons 1 0
def T.decode {α : Type} [Encodable α] : S → Option (T α)
| .cons 0 (.cons (.nat x) (.cons (.nat y) z)) =>
match Encodable.decode x, Encodable.decode y, T.decode z with
| some x, some y, some z => some <| T.a x y z
| _, _, _ => none
| .cons 1 0 => some <| T.b
| _ => none
lemma T.decode_encode {α : Type} [Encodable α] (t : T α) : T.decode (T.encode t) = some t := by
cases t <;> simp [T.encode, T.decode, T.decode_encode]
instance {α : Type} [Encodable α] : Encodable (T α) where
encode t := Encodable.encode (T.encode t)
decode n := Encodable.decode n >>= T.decode
encodek := by simp [T.decode_encode]
Eric Wieser (Nov 02 2024 at 23:03):
S is FreeMagma Nat
, right?
Eric Wieser (Nov 02 2024 at 23:03):
(though maybe that's not a helpful tool to use!)
Kyle Miller (Nov 03 2024 at 00:01):
Here are Encodable
derived handlers too: #18564
Kyle Miller (Nov 03 2024 at 00:03):
It doesn't support indexed inductive types. That'll be some extra work; the issue is in the decoding function. I think what you'd do is have it use DecidableEq to do equality tests with the indices, and use those successful tests to rewrite the indices.
Kyle Miller (Nov 03 2024 at 00:04):
(I'm not sure how observing that binary trees of natural numbers is the same as FreeMagma Nat
helps here @Eric Wieser, sorry)
Violeta Hernández (Nov 03 2024 at 00:05):
Nice!
Violeta Hernández (Nov 03 2024 at 00:05):
Doesn't this subsume the Countable
handler, though?
Violeta Hernández (Nov 03 2024 at 00:07):
If you have e.g. Encodable a -> Encodable (List a)
then you can derive Countable a -> Countable (List a)
by nonconstructively building an Encodable
instance, which then gives you the Countable
instance you want
Kyle Miller (Nov 03 2024 at 00:07):
No, because this needs Encodable instances for its parameters
Kyle Miller (Nov 03 2024 at 00:08):
You can derive that, but it's not automatic with typeclass synthesis
Violeta Hernández (Nov 03 2024 at 00:08):
Yeah, that's what I mean
Kyle Miller (Nov 03 2024 at 00:08):
This is why it doesn't subsume it
Violeta Hernández (Nov 03 2024 at 00:09):
Perhaps "simplify" is the correct word then
Kyle Miller (Nov 03 2024 at 00:09):
The stronger reason at the moment is that the Countable handler can deal with indexed inductive types
Violeta Hernández (Nov 03 2024 at 00:09):
Oh, alright
Kyle Miller (Nov 03 2024 at 00:09):
The Countable handler is also simpler and quicker-to-elaborate
Violeta Hernández (Nov 03 2024 at 00:09):
Perhaps this can be left as a TODO then? or maybe not if the difference in performance is significant
Violeta Hernández (Nov 03 2024 at 00:10):
Hope I'm not coming off as nitpicky, this is really cool in any case
Kyle Miller (Nov 03 2024 at 00:17):
I don't think it's really necessary, it's not like in the math library where generalization is good, where everything can be type-checked for mathematical correctness. Meta code is only as correct as the testing we do. I feel that some of the automation in the Encoding handler for the proofs is sketchier than the Countable one.
I think making the Countable handler be in terms of the Encodable handler would make it more complicated and harder-to-maintain overall. This also frees us from needing to design it with any consideration for computability.
Kyle Miller (Nov 03 2024 at 00:24):
It would be neat to re-use some of the Countable handler code for a Finite handler though. The idea would be to use the fact that if you have an injection to Nat from a Finite type, the function is bounded, and then in all the Nat.pair
constructions, we can deduce that the constructed function is bounded as well.
Or there's reusing the Fintype deriving handler machinery, which would work fine too, though that doesn't handle indexed inductives.
Violeta Hernández (Nov 03 2024 at 00:26):
Fair enough. I think these were just my math instincts kicking in :stuck_out_tongue:
Kyle Miller (Nov 03 2024 at 00:30):
In another direction, it might be interesting having an Infinite
derive handler that creates one or more instances that would prove the type is infinite, like
structure Pair (α β : Type*) where
fst : α
snd : β
deriving Infinite
-- creates
instance [Infinite α] : Infinite (Pair α β) := ...
instance [Infinite β] : Infinite (Pair α β) := ...
Violeta Hernández (Nov 03 2024 at 00:32):
Would be nice. I think this handler is perhaps lower priority, since it's usually not too difficult to build something like an injection from N.
Violeta Hernández (Nov 03 2024 at 00:35):
(in fact, many types have a preferred injection!)
Edward van de Meent (Nov 03 2024 at 08:43):
You'd also need assumptions about Nonempty
for this example, but that does seem like an interesting idea
Edward van de Meent (Nov 03 2024 at 08:45):
Btw, is there a good resource for writing derive handlers? I think I'd like to get more familiar with this part of lean...
Edward van de Meent (Nov 03 2024 at 11:25):
also, does it make sense to adapt this to derive Primcodable
instances?
Edward van de Meent (Nov 03 2024 at 11:40):
(because from what i can tell, this should be doable)
Edward van de Meent (Nov 03 2024 at 13:23):
from the looks of it, we'd need a variant of docs#Enumerable.ofLeftInjective for docs#Primcodable ...
Kyle Miller (Nov 03 2024 at 15:33):
@Edward van de Meent Hints:
- Most deriving handlers are basically macros. They construct command syntax using knowledge of the inductive types and then ask the command elaborator to do the rest. A minority create definitions from scratch and add them to the environment directly.
- The
Deriving
namespace has some functions that are convenient for certain types of deriving handlers, but don't expect them to do everything you want. You may need to copy functions into your deriving handler and modify them to suit your needs. (TheCountable
deriving handler had to do this with theinstance
-creation function for example.) - There's no documentation about any of this really. You can try to find a pre-existing deriving handler that is similar to what you want to do. You'll need to read some core source code in this
Deriving
namespace and try to figure out what it does and what it's for. - Don't try writing a handler from scratch, start with a related one. With
Countable
, I copy/pasted relevant bits from theDecidableEq
andRepr
handlers, probably some others too, and then hammered it into shape. For this one, I needed to generate additional names for auxiliary declarations and thread these around since the Deriving.Context assumes you only need a single kind of aux declaration. (It would probably be cleaner to abandon the Deriving namespace at this point and make something custom forCountable
!)
Kim Morrison (Nov 05 2024 at 00:15):
Kyle Miller said:
In another direction, it might be interesting having an
Infinite
derive handler that creates one or more instances that would prove the type is infinite, likestructure Pair (α β : Type*) where fst : α snd : β deriving Infinite -- creates instance [Infinite α] : Infinite (Pair α β) := ... instance [Infinite β] : Infinite (Pair α β) := ...
Just noting the existing PR #3610 from @Timothy Gu.
Last updated: May 02 2025 at 03:31 UTC