Zulip Chat Archive
Stream: lean4
Topic: New LT instance
Gabriel Ebner (May 30 2021 at 16:10):
Lean 4 just got a second instance for LT Nat
that is not definitionally equal to the first one. I was just starting to jot down some notes for a pre-RFC, but I wasn't sure if there's a nice way to refactor this.
Clearly you want to avoid writing both Ord and LT instances. For programming, it's obviously a really bad idea to derive Ord from LT. And you can't define LT in terms of Ord either (it's not possible with partial orders in general; and using it as a default results in diamonds with e.g. pi instances).
The typical one-size-fits-all mathlib answer would be the following, right?
class Ord (α : Type u) [LT α] [BEq α] where
compare : α → α → Ordering
compare_correct : ∀ a b : α, -- not sure how helpful this is w/o laws on < and ==
open Ordering in match compare a b with
| lt => a < b
| eq => a == b
| gt => a > b
But that doesn't look helpful for Ord/LT. Is there a reasonable alternative to just removing the Ord α → LT α
instance? Ideally one that addresses the motivation for this instance, i.e., not having to write a separate LT instance? Making it a def instead (but then you still need to write both instances)?
Daniel Fabian (May 30 2021 at 16:33):
I was thinking about the one you just described with BEq
, when I was adding Ord
, but I wasn't happy with it. I was also considering to do it using DecidableEq
. But that wasn't truly satisfying either.
In the end, I just made explicit Ord
instances and added the helper compareOfLessAndEq
this latter one is definitely safe, since you control when it's applied. And for basic data types like number or chars, it certainly is reasonable to derive Ord
from that. But for other more complex types, you'd want to derive it explicitly, avoiding to redo work.
Furthermore, Ord
seems to imply a total order, but that's not quite right. In particular Eq a b
doesn't imply a = b
.
Daniel Fabian (May 30 2021 at 16:37):
on the other hand, if we had a total order, deriving an Ord
instance for that seems perfectly reasonable, what do you think?
Gabriel Ebner (May 30 2021 at 16:40):
Daniel Fabian said:
on the other hand, if we had a total order, deriving an
Ord
instance for that seems perfectly reasonable, what do you think?
We want to avoid that for programming; an Ord instance defined in terms of < is never going to be fast (with the few exceptions that you've listed).
Gabriel Ebner (May 30 2021 at 16:41):
(The lexicographic order on strings is a decidable linear order. But you don't want to define an Ord instance from it.)
Daniel Fabian (May 30 2021 at 16:49):
Gabriel Ebner said:
Daniel Fabian said:
on the other hand, if we had a total order, deriving an
Ord
instance for that seems perfectly reasonable, what do you think?We want to avoid that for programming; an Ord instance defined in terms of < is never going to be fast (with the few exceptions that you've listed).
which is precisely, why I added a helper function to construct it for the 20 or so cases, but haven't added a typeclass instance.
Sebastian Ullrich (May 30 2021 at 17:58):
How do the two instances for Nat
compare? Does the old one have important defeqs the new one does not, e.g. n < m === n.succ <= m
? (Is this one important?)
Gabriel Ebner (May 30 2021 at 18:00):
The problem is that there's two of them now, i.e. n < m =!= n < m
Sebastian Ullrich (May 30 2021 at 18:17):
Sure, but I'm asking whether we need the former one :big_smile:
Gabriel Ebner (May 30 2021 at 18:40):
IIRC @Floris van Doorn had some strong opinions on this. But I don't think there's any technical requirement for nice definitional reduction of <.
Gabriel Ebner (May 30 2021 at 18:51):
Removing the other LT Nat
does not fix the issue though (I've just picked the most prominent instance for the thread title). The Ord α → LT α
instance causes diamonds even if all the duplicate LT instances for types in the core library are fixed, and we adopt the policy that every concrete type only has Ord
defined and not LT
(where possible).
Blanket instances constructing data like Ord α → LT α
always cause diamonds. Say you make a type LexPair α
for lexicographically ordered pairs. You add an instance for Ord α → Ord (LexPair α)
because you want your code to be fast. Then you add LT α → LT (LexPair α)
because the lexicographic ordering also makes sense for types that do not have an Ord instance (e.g. if they're not totally ordered). And bam: you've got two instances LT (LexPair α)
that are not definitionally equal.
Daniel Fabian (May 30 2021 at 18:58):
do you have a suggestion? That was something that was always annoying me about type classes. You don't even need a diamond, actually. Say, you have string comparison and case insensitive string comparison. Both are perfectly reasonable things to do. But the type class mechanism is based just on types, so there is really no sensible way to tell which instance you want applied, is there?
Mario Carneiro (May 30 2021 at 19:01):
Sebastian Ullrich said:
How do the two instances for
Nat
compare? Does the old one have important defeqs the new one does not, e.g.n < m === n.succ <= m
? (Is this one important?)
Yes, this defeq is used a lot in mathlib.
Mac (May 30 2021 at 19:01):
Gabriel Ebner said:
Then you add
LT α → LT (LexPair α)
because the lexicographic ordering also makes sense for types that do not have an Ord instance (e.g. if they're not totally ordered).
Unless I'm mistaken though, Ord
is not a total order. You can define and Ord instance for any type that has an decidable LT. For example,
def compareOfLess {α} [LT α] [DecidableRel (@LT.lt α _)] (x y : α) : Ordering :=
if x < y then Ordering.lt
else if x > y then Ordering.gt
else Ordering.eq
instance [LT α] [DecidableRel (@LT.lt α _)] : Ord α :=
⟨ compareOfLess ⟩
In order to promote Ord
to a true total order, a ((x <=> y) = Ordering.Eq) -> (x = y)
law would need to be added.
Gabriel Ebner (May 30 2021 at 19:03):
Yes, these are all lawless. For illustrative purposes, I only picked examples where the instances are lawful though. Because the underlying point is the same no matter whether there are laws or not: blanket instances that construct data cause diamonds.
Mario Carneiro (May 30 2021 at 19:03):
My 2 cents: I think that the Ord -> LT instance should be a def, and instantiated explicitly for any types where the simplest way to prove LT is via Ord.
Mac (May 30 2021 at 19:05):
I think it might be worth it to demote the current Ord
to POrd
(i.e. a partial order) with Ordering := LT | GT | NA
and then introduce a new Ord
that has the ((x <=> y) = Ordering.NA) -> (x = y)
law. That way any all LT
/GT
simply leverage POrd
(if they have a decidable LT/GT ordering relation).
Gabriel Ebner (May 30 2021 at 19:05):
Daniel Fabian said:
string comparison and case insensitive string comparison [...] But the type class mechanism is based just on types, so there is really no sensible way to tell which instance you want applied, is there?
I think the cleanest way is to make type aliases, i.e., def LocaleSpecificSortingAccordingToUnicode (locale : String) : Type := String
.
Mario Carneiro (May 30 2021 at 19:11):
Note that the idea of a function returning LT | GT | EQ already bakes in the concept of a "total preorder" for things like lt
and cmp
to cohere and be useful in data structures
Mac (May 30 2021 at 19:11):
hence why I suggested the rename to something more general
Mario Carneiro (May 30 2021 at 19:12):
It's not a partial order though
Mario Carneiro (May 30 2021 at 19:12):
Ord
has signature A -> A -> Ordering
where Ordering := LT | GT | EQ
Mario Carneiro (May 30 2021 at 19:13):
EQ
doesn't necessarily mean equal, but it does mean comparable. A partial order would need a none
option
Mac (May 30 2021 at 19:16):
Sorry for my imprecision. The point is pick a name that neither implies incomparability or comparability (i.e. Ordering := Less | Greater | Neutral
and DecidableOrd
for example) and use this more general Ord to always define decidable LT
/GT
.
Mario Carneiro (May 30 2021 at 19:17):
See Rust's PartialOrd
: it needs to be a fourth option
Gabriel Ebner (May 30 2021 at 19:17):
I believe partial orders are a red herring when it comes to Ord instances. We only want Ord instances where the order needs to be total (sorting a list, keys in a tree map, etc.). What is a difficulty though is that Ordering.eq
does not mean =
, it rather means ==
(there's of course no law requiring it). For example Ord Expr
would return eq
for alpha-equivalent expresisons.
Mario Carneiro (May 30 2021 at 19:17):
I think it would be best to introduce a Cmp
typeclass that just supplies the cmp : A -> A -> Ordering
function, and then Ord
can be reserved for the trifecta, possibly with laws relating them
Mac (May 30 2021 at 19:19):
Mario Carneiro said:
I think it would be best to introduce a
Cmp
typeclass that just supplies thecmp : A -> A -> Ordering
function, and thenOrd
can be reserved for the trifecta, possibly with laws relating them
This is more-or-less what I was trying to suggest.
Mario Carneiro (May 30 2021 at 19:19):
in particular I'm not suggesting "and use this more general Ord to always define decidable LT/GT."
Mario Carneiro (May 30 2021 at 19:20):
There is no a priori relation between the basic typeclasses
Mario Carneiro (May 30 2021 at 19:21):
Ord
would combine several ordering operators for convenience: BEq
, LT
, LE
, Cmp
, Decidable LE
, Decidable LT
Mario Carneiro (May 30 2021 at 19:22):
POrd
would have a partial_cmp : A -> A -> Option Ordering
instead
Mac (May 30 2021 at 19:23):
Mario Carneiro said:
There is no a priori relation between the basic typeclasses
Why Not Though? From Cmp
I can always get Lt
/Le
/Ge
/Gt
.
Mario Carneiro (May 30 2021 at 19:24):
Building data from data leads to the diamond problems that gabriel mentioned
Mario Carneiro (May 30 2021 at 19:24):
you don't want to use a generic construction to get Lt from Cmp when the base type is Nat
, because nat already has a Lt
Mac (May 30 2021 at 19:25):
But why can't we remove those and just use Cmp
for Cmp
types?
Mario Carneiro (May 30 2021 at 19:25):
So the generic construction is just a def, available as the default field value in the Ord
typeclass
Mario Carneiro (May 30 2021 at 19:27):
Mac said:
But why can't we remove those and just use
Cmp
forCmp
types?
because that's less efficient? Also it's less convenient when you don't want cmp to get involved in proofs
Mario Carneiro (May 30 2021 at 19:29):
Some types have lt
as the basic notion and use it to define le
and cmp
; some have cmp
as basic and define lt
and le
; others (like Nat
!) have lt
and le
as basic and cmp
is derived. When you have this kind of variety, you just need a class that supplies all of them as separate fields and you can pick which to define yourself and which to get automatically filled in
Mario Carneiro (May 30 2021 at 19:33):
Actually, it's even more complicated than that because there is both le
and ble
and they have separate definitions. (In mathlib le
is an inductive predicate and ble
is a recursive function.) Again, to please everyone you need them to be separate fields, with a coherence lemma
Mac (May 30 2021 at 19:33):
Mario Carneiro said:
When you have this kind of variety, you just need a class that supplies all of them as separate fields and you can pick which to define yourself and which to get automatically filled in
That is what I am going for. For example, something like this:
class BOrd.{u} (α : Type u) extends Cmp α, LT α, LE α, GT α, GE α where
lt (a b : α) := cmp a b == Ordering.lt
le (a b : α) := cmp a b != Ordering.gt
gt (a b : α) := cmp a b == Ordering.gt
ge (a b : α) := cmp a b != Ordering.lt
eq (a b : α) := cmp a b == Ordering.eq
Mario Carneiro (May 30 2021 at 19:34):
oof, I really hope I don't have to worry about GT
too
Mac (May 30 2021 at 19:36):
Mario Carneiro said:
oof, I really hope I don't have to worry about
GT
too
But the current definition of GT creates the same inefficiencies that are currently being railed against (flip
is less efficient then a type that directly supports GT
).
Mario Carneiro (May 30 2021 at 19:37):
GT
doesn't create diamonds because we actually have a decent chance of actually ensuring that no alternate definitions are made
Mario Carneiro (May 30 2021 at 19:38):
also if you inline flip
it shouldn't be any slower
Mac (May 30 2021 at 19:38):
It may not create diamonds but it does produce inefficient code (which is the reason why diamonds are bad).
Mario Carneiro (May 30 2021 at 19:38):
or better yet, just don't use GT
Mac (May 30 2021 at 19:39):
Mario Carneiro said:
also if you inline
flip
it shouldn't be any slower
Not if the source arguments are already in the opposite order.
Mac (May 30 2021 at 19:40):
If a I have foo a b := lt b a
this will sometimes necessitate spending cycles to swap the argument order; foo := gt
will never.
Mario Carneiro (May 30 2021 at 19:41):
Working with closures is a lot slower than either one
Mario Carneiro (May 30 2021 at 19:41):
always fully apply your functions if possible
Mac (May 30 2021 at 19:42):
I am pretty sure foo := gt
is just as efficient as foo a b := gt a b
(if not more so)
Mario Carneiro (May 30 2021 at 19:43):
they are compiled to the same thing, it looks like foo(a, b) { return gt(a, b) }
when it gets translated to C
Mario Carneiro (May 30 2021 at 19:44):
and argument swapping is free at that point
Mac (May 30 2021 at 19:44):
Mario Carneiro said:
always fully apply your functions if possible
Are you sure about this? This is consider bad in most functional languages. I know this is definite no no in Haskell. I don't know enough about Lean's compiler to know if this is true in Lean though.
Mario Carneiro (May 30 2021 at 19:45):
Haskell has to do crazy things to put the variables back in. Pointfree style is just a design aesthetic
Mac (May 30 2021 at 19:45):
Mario Carneiro said:
and argument swapping is free at that point
no its not?
foo(a, b) { return gt(a, b) }
is faster than foo(a, b) { return lt(b, a) }
in C.
In fact, it is much faster in the standard C calling conventions as the latter requires swapping things on the stack.
Mario Carneiro (May 30 2021 at 19:45):
if haskell didn't inline so heavily it wouldn't be a reasonable option at all
Mario Carneiro (May 30 2021 at 19:46):
That function would not ever be called, it would be inlined
Mario Carneiro (May 30 2021 at 19:47):
swapping is free here because it's just changing the way the inlining goes
Mac (May 30 2021 at 19:49):
Mario Carneiro said:
Haskell has to do crazy things to put the variables back in. Pointfree style is just a design aesthetic
This is not true. I clearer remember a number of situations in a Haskell where pointfree explicitly provides performance benefits. Its been a while since I've used Haskell, so I don't remember the specifics. But, for thing, I think Haskell as to evaluate the function's thunk in to in order to apply it so I think applications hurt laziness.
Mario Carneiro (May 30 2021 at 19:51):
It's possible that haskell's evaluation semantics can cause a complication here, but you can still write in fully applied form if you evaluate the functions outside the lambda
Mac (May 30 2021 at 19:51):
Mario Carneiro said:
That function would not ever be called, it would be inlined
You are trusting inliners way to much. Second, it is completely possible that lt
could be function pointer or a closure or something that is non-inlinable. foo
could also be complex enough that it could not be inlined (or it could be being linked to in a manner that prevents inlining).
Mac (May 30 2021 at 19:54):
Mario Carneiro said:
It's possible that haskell's evaluation semantics can cause a complication here, but you can still write in fully applied form if you evaluate the functions outside the lambda
I am not arguing you can write in fully-applied form (and that there are probably cases where it is a good idea), but simply that (1) writing fully applied functions in Haskell is very bad style (in particular, linters will complain heavily about it) and (2) there are many cases where it is a performance problem and (3) it can also damage laziness
Mario Carneiro (May 30 2021 at 19:54):
You are trusting inliners way too much.
Functional programming languages depend on copious optimizations to work. The performance characteristics of a program change completely if you turn off the optimizer. Inlining in particular is really really important, and if it doesn't work then that's one of the first places to look for a performance issue.
Anyway, like I said the easiest solution is to just never use gt
. It never comes up outside contrived situations like foo
(which is literally just gt
) which would be optimized.
Mac (May 30 2021 at 19:56):
Mario Carneiro said:
Anyway, like I said the easiest solution is to just never use
gt
. It never comes up outside contrived situations likefoo
(which is literally justgt
) which would be optimized.
I disagree greatly with this. Having played around with compilers a lot, I can tell you there are many cases where argument reordering incurs performance penalties. Optimizers can only do so much.
Mac (May 30 2021 at 19:56):
Furthemore, at a notational level I may wish to define >
for type without wanting to define <
.
Mario Carneiro (May 30 2021 at 19:58):
It's very difficult to discuss this in the abstract, because there are so many confounding factors. It would need a full code example to say something more specific. Regarding notation, you can do that with macro magic, as ever.
Mario Carneiro (May 30 2021 at 19:59):
I fully intend to keep up the mathlib 3 ban on gt
in mathlib 4
Mac (May 30 2021 at 20:00):
also I fail to see what the overhead in GT
type class would be anyway
Mario Carneiro (May 30 2021 at 20:00):
the overhead of GT
is the fact that the typeclass exists, and you have to relate the functions and duplicate all lemmas
Mac (May 30 2021 at 20:01):
Mario Carneiro said:
I fully intend to keep up the mathlib 3 ban on
gt
in mathlib 4
I think this is another good example of why alternative to mathlib will definitely pop up if/when Lean matures. It is far to opinionated to not acquire competition.
Mario Carneiro (May 30 2021 at 20:02):
perhaps. I have yet to see an actual reason to lift the ban though
Mac (May 30 2021 at 20:02):
Mario Carneiro said:
the overhead of
GT
is the fact that the typeclass exists, and you have to relate the functions and duplicate all lemmas
Can't you just elide the duplication with a a > b = b < a
simp
lemma?
Mario Carneiro (May 30 2021 at 20:02):
presumably some alternately-opinionated mathlib alternative would be able to supply such a reason
Mario Carneiro (May 30 2021 at 20:03):
Sure, but it still means the overhead of using simp more, supplying that lemma, larger proofs as a result, interaction between defeq and simp, etc etc
Mario Carneiro (May 30 2021 at 20:03):
The best code is the code you don't have to write
Mac (May 30 2021 at 20:05):
I just think you and I are just not going to agree on design standards anytime soon. :rolling_on_the_floor_laughing:
Mac (May 30 2021 at 20:05):
Except, oddly, I guess, for leanpkg
.
Mario Carneiro (May 30 2021 at 20:05):
feel free to make a mathlib alternative and make it public
Gabriel Ebner (May 31 2021 at 07:08):
Mario Carneiro said:
the overhead of
GT
is the fact that the typeclass exists, and you have to relate the functions and duplicate all lemmas
Note that GT
is not a typeclass, and wasn't in Lean 3 either. (Only God knows why it's nevertheless called GT.gt
...) IIRC the only complications w.r.t. gt
was that it broke simp and rw (due to head-symbol indexing), but could we write ε > 0
again if that's fixed?
Sebastian Ullrich (May 31 2021 at 07:50):
I wasn't quite sure myself, but it does currently work:
example (h : 0 < ε) : ε > 0 := by simp [h]
example (h : ε > 0) : 0 < ε := by simp [h]
This is because both patterns and queries are recursively brought into (reducible) whnf, before inserting/comparing them against each other in the discrimination tree (so you don't pay for all potential matches for each subexpression like in Lean 3)
Daniel Fabian (May 31 2021 at 09:12):
Gabriel Ebner said:
I believe partial orders are a red herring when it comes to Ord instances. We only want Ord instances where the order needs to be total (sorting a list, keys in a tree map, etc.). What is a difficulty though is that
Ordering.eq
does not mean=
, it rather means==
(there's of course no law requiring it). For exampleOrd Expr
would returneq
for alpha-equivalent expresisons.
It doesn't even mean ==
, it just means not <
and not >
. I was struggling with this when I tried to relate Ord
, LT
and BEq
or DecidableEq
.
Or, more precisely, that's how it's used in the data structures.
Gabriel Ebner (May 31 2021 at 09:16):
Right it could be something different, so you shouldn't accidentally use ==
instead. What I wanted to say is that Ordering.eq
is usually an equivalence relation and in general not the same as =
(and in this way it is similar to ==
).
Mario Carneiro (May 31 2021 at 09:16):
That comes back to the "total preorder" business mentioned earlier: you need x < y \/ x == y \/ x > y
for the data structure to work
Mario Carneiro (May 31 2021 at 09:17):
that's what justifies using not < and not >
as synonymous for ==
Daniel Fabian (May 31 2021 at 09:18):
and I was saying, that we don't rely x == y
. It's a bit of an academic discussion, but it may be relevant in some of the more complicated types like Expr
.
Mario Carneiro (May 31 2021 at 09:19):
You can use just <
if you know that the ordering is induced by it, although using cmp
is usually faster for the data structure
Mario Carneiro (May 31 2021 at 09:20):
it means you only need a [LT A]
typeclass in the data structure instead of the whole zoo implied by Ord
Daniel Fabian (May 31 2021 at 09:20):
yeah, that's the refactoring we did.
Daniel Fabian (May 31 2021 at 09:20):
I took the LT
implementation, and replaced it with Ord
.
Daniel Fabian (May 31 2021 at 09:21):
But Eq
is just the fallback case, essentially.
Daniel Fabian (May 31 2021 at 09:22):
Ord
is a cheap way of getting < a
and > a
in one operation.
Mario Carneiro (May 31 2021 at 09:22):
I think the fact that these are lawless structures is confusing matters. The laws required for the data structure to work include transitivity, stuff like x < y -> !(x == y)
, and !(x < y) -> !(y < x) -> x == y
Mario Carneiro (May 31 2021 at 09:22):
just because they aren't literally used in the programming version doesn't mean they don't conceptually exist anyway
Mario Carneiro (May 31 2021 at 09:23):
it's mostly just simpler to leave the laws out since it means you don't have to prove anything while writing programs
Daniel Fabian (May 31 2021 at 09:24):
Yeah, I get it. And in principle it would be nice to have some kind of lemma carried around with Ord
instances. But, alas, that's not the case currently.
Mario Carneiro (May 31 2021 at 09:24):
but the flip side of that is that it is safe to assume that when the laws don't hold, it is "undefined behavior", i.e. the data structure does something but we don't really care what
Mario Carneiro (May 31 2021 at 09:25):
(Personally, I would like to see more access to actual C undefined behavior, but that's a whole separate discussion.)
Daniel Fabian (May 31 2021 at 09:26):
It'd be nice to have a conditional proof, though. Something like "if the instance is lawful, then the RBSet behaves like a set" or something like that.
Mario Carneiro (May 31 2021 at 09:26):
Sure, that's something that mathlib could prove even if the lawless data structure itself is in lean core
Daniel Fabian (May 31 2021 at 09:27):
Nice
Mario Carneiro (May 31 2021 at 09:31):
Speaking of which, I've thought of a simple way to avoid the issue with having the wrong invariants in things like RBNode.WellFormed
(they are too strict, they should talk about the black height): Add another constructor to WellFormed
using the full red-black invariant. It doesn't ever have to be used in lean core, the only requirement is that the predicate itself is defined. Then mathlib et al can prove that the inductive type "generated by insert and erase and empty and the red-black invariant" is equivalent to "the red-black invariant" by proving that insert, erase, and empty preserve the red-black invariant. The upside is that it then becomes possible to define operations directly on red-black trees that don't go via insert/erase and satisfy the red-black invariant for a different reason
Mario Carneiro (May 31 2021 at 10:08):
Here's what the PR would look like:
+def blackHeight : RBNode α β → OptionM Nat
+ | leaf => pure 0
+ | node red l k v r => do
+ let n ← blackHeight l
+ guard (!isRed l && !isRed r && (← blackHeight r) == n)
+ pure n
+ | node black l k v r => do
+ let n ← blackHeight l
+ guard (!isRed l && (← blackHeight r) == n)
+ pure (n + 1)
+
+def ordered (cmp : α → α → Ordering) : RBNode α β → Bool
+ | leaf => true
+ | node _ l k _ r =>
+ all (fun x _ => cmp x k == Ordering.lt) l &&
+ all (fun y _ => cmp k y == Ordering.lt) r &&
+ ordered cmp l && ordered cmp r
inductive WellFormed (cmp : α → α → Ordering) : RBNode α β → Prop where
| leafWff : WellFormed cmp leaf
| insertWff {n n' : RBNode α β} {k : α} {v : β k} : WellFormed cmp n → n' = insert cmp n k v → WellFormed cmp n'
| eraseWff {n n' : RBNode α β} {k : α} : WellFormed cmp n → n' = erase cmp k n → WellFormed cmp n'
+ | balancedWff {n : RBNode α β} : (blackHeight n).isSome → ordered cmp n → WellFormed cmp n
Daniel Fabian (May 31 2021 at 10:19):
That doesn't feel like a good idea. The current predicate simply asserts that insertion is well-formed. Almost like an axiom. We should instead formulate the 5 invariants and show that the algorithm preserves them. Then the current predicate would simply assert that an rb tree was constructed by insertions. And your theorem would thus prove the invariants.
And you could define union or intersection etc.
Mario Carneiro (May 31 2021 at 10:21):
I would of course prefer to just prove the correctness of the operations. The advantage of this version is that the lean devs don't have to, it can be offloaded to a library like mathlib. As it currently is written it's not possible to prove this because the actual invariant as stated does not match the real red black invariant
Daniel Fabian (May 31 2021 at 10:25):
What if lean you take the predicate and show that it implies the invariants? As is, you know a tree came into existence as a sequence of applications of those 3 functions
Mario Carneiro (May 31 2021 at 10:26):
The "minimal" well formedness predicate is the inductive type generated by the operations you want to do on the data structure. The "maximal" well formedness predicate is the list of properties you expect (ordering, balance). The hard proof work that can be shifted around but not eliminated is showing that the minimum is less than the maximum. The predicate defined here is basically the larger of these two predicates, so it's obviously greater than the minimal one (so the operations are easy to define) and it's equivalent to the maximal predicate once you do the hard proof work
Mario Carneiro (May 31 2021 at 10:28):
As it is currently, it is possible to prove that WellFormed
implies ordering and balance, but not vice versa (I'm not positive but I think the converse is false)
Mario Carneiro (May 31 2021 at 10:30):
Adding the balancedWff
variant makes this true by fiat, and the hard proof work becomes showing that everything else in the WellFormed
inductive type is superfluous
Daniel Fabian (May 31 2021 at 10:50):
There are 5 rb invariants. I'm pretty sure you need all of them in the induction. Also, it's an rb tree with all 5.
The predicate as is will create a provably well formed tree without proving it. If you formulate the 5 invariants, the current predicate should imply the 5 invariants.
The converse won't hold, because you clearly can create a tree however you like.
And only asserting a subset of the 5 invariants would be an rb tree anymore.
Mario Carneiro (May 31 2021 at 11:50):
what 5 invariants? I think ordering and balance, defined above, cover it
Mario Carneiro (May 31 2021 at 11:51):
If there are more invariants, yes they should show up (conjoined in that last balanceWff
variant, not as separate constructors)
Mario Carneiro (May 31 2021 at 11:52):
The main point I'm making is that this can be done with little impact on the code besides the need to write down what the invariant is; no proofs are required in lean core
Mario Carneiro (May 31 2021 at 11:57):
The converse won't hold, because you clearly can create a tree however you like.
It's a nontrivial theorem to show that there are trees that satisfy the invariants without being created by some sequence of insertions and deletions (or that there aren't). It's certainly not obvious to me whether it holds or not - you can get quite a lot of trees by carefully ordering the insertions and maybe getting unusual color layouts by adding and removing other values.
Mario Carneiro (May 31 2021 at 11:58):
still, even if the given predicate is correct, because all well formed trees are accessible by some sequence of inserts and deletes, it seems like an unnecessarily hard theorem to deal with when simply stating the real predicate would solve the problem
Mario Carneiro (May 31 2021 at 12:04):
oh, I think the red black trees as implemented also have a black root node, so you need that too
| balancedWff {n : RBNode α β} : !isRed n → (blackHeight n).isSome → ordered cmp n → WellFormed cmp n
Daniel Fabian (May 31 2021 at 12:05):
Oh yes, I totally get where you're coming from. On the other hand, I don't see what's hard to prove? Just make a copy of insert
call it insert'
and you've just given a counterexample? I.e. a different way to build a valid tree.
Or did you mean to prove that any valid tree is somehow accessible as a construction of the aforementioned operations. Which I agree is non-obvious to say the least. Although, I don't see why you would care? Generally once a tree is valid, you get all the good properties like fast lookups, etc.
The are the coloring invariants, the root color invariant, the height invariant, sortedness invariant and the leaf color invariant. All 5 have to hold.
Mario Carneiro (May 31 2021 at 12:06):
Oh yes, I totally get where you're coming from. On the other hand, I don't see what's hard to prove? Just make a copy of insert call it insert' and you've just given a counterexample? I.e. a different way to build a valid tree.
No, because if insert' t x y = insert t x y
then it still satisfies insertWff
Mario Carneiro (May 31 2021 at 12:07):
similarly, even if the function isn't defined using insert and erase, as long as it is provably equal to some sequence of insert and erase it is still well formed by the current predicate
Mario Carneiro (May 31 2021 at 12:08):
Although, I don't see why you would care? Generally once a tree is valid, you get all the good properties like fast lookups, etc.
The issue is that all operations on RBTree
are gated behind this WellFormed
predicate that is defined in terms of being accessible by insert and erase. If WellFormed
contained the balancedWff
variant then you could easily get around the need to prove some complicated accessibility thing by proving well formedness the normal way
Daniel Fabian (May 31 2021 at 12:09):
Ok, so consider an insertion algorithm that, say always replaces x
with y
and one that keeps it in the Ordering.Eq
case. Now you have two different trees that are both valid, but distinct. And one is never constructable from one algorithm and the other is.
(We are talking about a case where x = y
is not true, but Ordering.Eq x y
is.
Mario Carneiro (May 31 2021 at 12:09):
(note that I'm talking about additions to the RBTree
data structure outside the core, where changing the definition of WellFormed
isn't an option)
Daniel Fabian (May 31 2021 at 12:09):
Mario Carneiro said:
Although, I don't see why you would care? Generally once a tree is valid, you get all the good properties like fast lookups, etc.
The issue is that all operations on
RBTree
are gated behind thisWellFormed
predicate that is defined in terms of being accessible by insert and erase. IfWellFormed
contained thebalancedWff
variant then you could easily get around the need to prove some complicated accessibility thing by proving well formedness the normal way
ok, that one, makes a lot of sense.
Daniel Fabian (May 31 2021 at 12:11):
but then you really want the full set of properties, imo
Daniel Fabian (May 31 2021 at 12:12):
and I'd drop the empty case, then.
Daniel Fabian (May 31 2021 at 12:12):
since that one trivially fulfills the complicated invariants.
Mario Carneiro (May 31 2021 at 12:12):
Yeah the empty case is easy to prove
Mario Carneiro (May 31 2021 at 12:13):
I checked wikipedia and it has a list of 5 properties but some are things like "every node is red or black" that don't need a mention
Mario Carneiro (May 31 2021 at 12:13):
I think this is the complete list of invariants
Daniel Fabian (May 31 2021 at 12:14):
that one not, but the color invariant definitely is needed
Daniel Fabian (May 31 2021 at 12:14):
i.e. red parents have black children.
Mario Carneiro (May 31 2021 at 12:14):
The blackHeight
function bakes that in with internal !isRed
checks
Mario Carneiro (May 31 2021 at 12:15):
It's also possible to write it as an inductive predicate, that might be a bit shorter/cleaner but this one is actually executable which might be useful
Mario Carneiro (May 31 2021 at 12:19):
like so
inductive Balanced : RBNode α β → Rbcolor → Nat → Prop
| leaf : Balanced leaf black 0
| red : Balanced l black n → Balanced r black n → Balanced (node red l k v r) red n
| black : Balanced l black n → Balanced r c n → Balanced (node black l k v r) black (n+1)
...
| balancedWff {n : RBNode α β} {k} : Balanced n black k → ordered cmp n → WellFormed cmp n
Daniel Fabian (May 31 2021 at 12:22):
yeah, that's more or less the option I used when doing the RBTree with types. You can encode those type families too. Makes writing the algorithm a huge pain, though. Doing it as a predicate on the side might be fine option.
Daniel Fabian (May 31 2021 at 12:22):
And if we had a Decidable
instance, we can make it executable that way, no?
Mario Carneiro (May 31 2021 at 12:24):
Yes, you can get the best of both worlds by adding decidability instances
Mario Carneiro (May 31 2021 at 12:25):
although blackHeight
is able to decide \exists k c, Balanced t c k
which is a bit less obvious with the inductive version
Mario Carneiro (May 31 2021 at 12:27):
For implementing the programming stuff, you wouldn't need to interact with these inductive types at all. It's only when you need to prove something is well formed that it comes up
Mario Carneiro (May 31 2021 at 12:27):
and for functions defined in core, they can cheat by using the other constructors and punt on the well formedness problem
Daniel Fabian (May 31 2021 at 12:28):
yes, if it's a predicate aside from the data structure. In my case, I had them weaved in. As in I had height index and a color index.
Daniel Fabian (May 31 2021 at 12:28):
now by construction you are balanced.
Daniel Fabian (May 31 2021 at 12:28):
but that means when you want to temporarily violate the invariants you need these intermediate types.
Daniel Fabian (May 31 2021 at 12:28):
it's not pretty.
Mario Carneiro (May 31 2021 at 12:29):
I've got an implementation of this stuff in a side project, complete with all the proofs
Mario Carneiro (May 31 2021 at 12:29):
you need some "broken invariant" invariants to deal with insert and erase
Daniel Fabian (May 31 2021 at 12:29):
yeah, pretty messy :-)
Mario Carneiro (May 31 2021 at 12:30):
but I like that the proposed approach means that core doesn't have to deal with any of that, it can be in mathlib where the supporting infrastructure for the proofs is more easily available
Daniel Fabian (May 31 2021 at 12:31):
sooo... how about we define the inductive pred, then and replace the empty ctor with one, where you give a proof of the predicate?
Mario Carneiro (May 31 2021 at 12:41):
Ditto for the WellFounded
predicates in HashMap
, HashSet
, BinomialHeap
, and PrefixTree
, which use a similar technique. LMK if you want me to send the invariants for these structures
Daniel Fabian (May 31 2021 at 12:54):
please make a bug for now and I'll bring up the context next time I have chat with leo in the next few days. After that, I think you can just make the pr, if that works for you.
Daniel Fabian (May 31 2021 at 12:58):
Just to be clear, with these additional cases in the WellFounded
predicates, we should be able to prove that lazy versions imply the invariant-based ones, correct?
Mario Carneiro (May 31 2021 at 12:59):
Yes, although those proofs will often require additional assumptions, for example that the cmp
operation is "lawful" (reflexive, transitive, symmetric)
Mario Carneiro (May 31 2021 at 12:59):
that's another good reason to keep it out of core
Daniel Fabian (May 31 2021 at 13:01):
then, that's actually quite nice... Because we show that the internal operations build up something that may or may not be a valid tree using applications of those algorithms. And if for a particular\alpha
and Ord \alpha
you know lawfulness, you can lift the statement into a proper well-formedness proof.
Daniel Fabian (May 31 2021 at 13:01):
I like it.
Mario Carneiro (May 31 2021 at 13:01):
I'll write up the issue
Daniel Fabian (May 31 2021 at 13:01):
thx
Last updated: Dec 20 2023 at 11:08 UTC