Zulip Chat Archive
Stream: lean4
Topic: Need suggestions to improve a DSL for dimensional calculus
Nicolas Rouquette (Jan 30 2022 at 00:55):
This is my first real lean4 program: https://github.com/NicolasRouquette/dimensional-calculus.lean4
I would like to document my journey in progressing from unproved object-oriented programming to proven functional programming style. I equate the former to the kind of lean4 code someone familiar with Java might write (well, perhaps a wee bit more than Java, perhaps oo-style Scala programming) and the latter to the kind of lean4 code that comes with verified proofs of useful properties.
This DSL domain, dimensional analysis, is mathematically very simple: equations involving variables combined using multiplications, divisions and rational powers of such. Using the CADE 2021 paper and the BoolExpr from the doc, I managed to make a basic DSL for this domain.
My questions are the following:
1) The DSL is not very pretty (look at Main.lean); it is unclear to me how to make it more elegant where I could parse a file of dimensional calculus formulas.
2) How should I organize my code w.r.t convenience functions like these below.
At some point, it does not make sense to bloat the standard library with such convenience functions.
Yet, these may be useful in a particular application. Where should I put these in a way that a reasonable reader might find them instead of adding them where I need them?
-- TODO: Where should these HashSet extensions be defined?
def ofList [BEq α] [Hashable α] (l : List α) : HashSet α :=
l.foldl (init := HashSet.empty) (fun m p => m.insert p)
def union [BEq α] [Hashable α] (h1: HashSet α) (h2: HashSet α) : HashSet α :=
h1.fold (init := h2) (fun h p => h.insert p)
def containsAll [BEq α] [Hashable α] (sup: HashSet α) (sub: HashSet α) : Bool :=
sub.fold (init := true) (fun b p => b && sup.contains p)
3) I have seen in the standard library comments like this:
-- TODO: remove `partial` using well-founded recursion
I also have 3 similar cases:
partial def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors
partial def applyDiv (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors
partial def substitute (ctx: Context) (fs: DCalcFactors) : DCalcFactors
How do we go about making these recursive functions 'well-founded' so that they would be total recursive functions?
I suspect that the answer depends, in part, in choosing the right kind of data structure that makes it easier for lean4 to reason about structural recursion.
4) Proving some properties about the program.
Is (3) necessary to prove properties about some data structures like DCalcFactors
?
I have been looking for examples in the lean4 source code and found something interesting in HashSet.lean
:
inductive WellFormed [BEq α] [Hashable α] : HashSetImp α → Prop where
| mkWff : ∀ n, WellFormed (mkHashSetImp n)
| insertWff : ∀ m a, WellFormed m → WellFormed (insert m a)
| eraseWff : ∀ m a, WellFormed m → WellFormed (erase m a)
def HashSet (α : Type u) [BEq α] [Hashable α] :=
{ m : HashSetImp α // m.WellFormed }
As the name suggests, it seems that HashSetImpl
is the implementation of HashSet
.
I am not entirely sure that I understand the elegance of formalizing well-formedness for hashsets.
Can someone explain this construction and proof?
- Nicolas.
Mario Carneiro (Jan 30 2022 at 02:21):
How do we go about making these recursive functions 'well-founded' so that they would be total recursive functions?
You haven't shown the definition of the function. The way to prove a definition well founded depends primarily on the ways in which you make recursive calls, and the structure of the domain type (here DCalcFactors
). It's pretty difficult to give advice without this
Mario Carneiro (Jan 30 2022 at 02:27):
Oh, you left a link. To prove a function is well founded, you need a "variant" that decreases on every recursive call. In the case of applyMul
, a suitable variant is fs2.size
. You would have to prove that fs2.size != 0 -> (HashMap.ofList fs2.toList.tail!).size < fs2.size
, which in theory should be combining some lemmas from the standard library
Mario Carneiro (Jan 30 2022 at 02:28):
The other two functions have exactly the same recursion structure, so you could use the same proof
Mario Carneiro (Jan 30 2022 at 02:30):
Is (3) necessary to prove properties about some data structures like
DCalcFactors
?
Yes, more or less. As far as the logic is concerned, any partial
function can have any behavior consistent with the type given to it. So for example there is no way you can prove that applyMul
is not the same as def applyMul (fs1 fs2 : DCalcFactors) := fs1
Mario Carneiro (Jan 30 2022 at 02:31):
Note that Lean 4 core is not overly concerned with proving correctness of its data structures. You are better off looking at mathlib4 if you are interested in that sort of thing
Mario Carneiro (Jan 30 2022 at 02:33):
For example, see BinaryHeap.lean which is a binary heap implementation which does not use partial
by using termination_by
where necessary
Mario Carneiro (Jan 30 2022 at 02:37):
For (2) your best bet is to put the functions in mathlib4. Most of us also have a for_mathlib
file or directory in our projects containing definitions and lemmas that aren't really project specific and really ought to be PR'd to mathlib but we haven't got around to it yet
Nicolas Rouquette (Jan 30 2022 at 19:08):
Mario Carneiro said:
Oh, you left a link. To prove a function is well founded, you need a "variant" that decreases on every recursive call. In the case of
applyMul
, a suitable variant isfs2.size
. You would have to prove thatfs2.size != 0 -> (HashMap.ofList fs2.toList.tail!).size < fs2.size
, which in theory should be combining some lemmas from the standard library
Thanks for the suggestion, I understand the idea:
show that the recursive calls are monotonically decreasing as a function of the size of the 2nd argument.
What I do not understand is how to express this in lean4; I tried your suggestion after refactoring
the code to make sure that both recursive calls use the same monotonically decreasing subset of the 2nd argument.
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors :=
let l2 : List DCalcFactor := fs2.toList
let f2 := l2.head!
let f2tail := HashMap.ofList l2.tail!
if 0 == fs2.size then
fs1
else
match fs1.getOp f2.fst with
| none =>
let fs1b := fs1.insert f2.fst f2.snd
applyMul fs1b f2tail -- tactic 'assumption' failed
| some (f1a : Lean.Rat) =>
let f12 : DCalcFactor := ⟨ f2.fst, f1a + f2.snd ⟩
let fs1a := fs1.erase f2.fst
let fs1b := if 0 == f12.snd.num then fs1a else fs1a.insert f12.fst f12.snd
applyMul fs1b f2tail
termination_by _ => fs2.size != 0 -> (HashMap.ofList fs2.toList.tail!).size < fs2.size
That there is no error on the 2nd match clause seems to be a consequence that the type checker stops at the 1st error.
Indeed, if I switch the order of the clauses, I get a similar error.
I wanted to write the termination criteria by referring to one of the let bindings, similar to what I've seen in Lean's
Init/Data/Array/Basic.lean:
def reverse (as : Array α) : Array α :=
let n := as.size
let mid := n / 2
let rec rev (as : Array α) (i : Nat) :=
if h : i < mid then
rev (as.swap! i (n - i - 1)) (i+1)
else
as
rev as 0
termination_by _ => mid - i
How is it that Lean4 in the above accepts a reference to a let binding, i.e., mid
, but in my case it does not:
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors :=
let l2 : List DCalcFactor := fs2.toList
let f2 := l2.head!
let f2tail := HashMap.ofList l2.tail!
let f2tailsize := f2tail.size
if 0 == fs2.size then
fs1
else
match fs1.getOp f2.fst with
| some (f1a : Lean.Rat) =>
let f12 : DCalcFactor := ⟨ f2.fst, f1a + f2.snd ⟩
let fs1a := fs1.erase f2.fst
let fs1b := if 0 == f12.snd.num then fs1a else fs1a.insert f12.fst f12.snd
applyMul fs1b f2tail -- tactic 'assumption' failed
| none =>
let fs1b := fs1.insert f2.fst f2.snd
applyMul fs1b f2tail -- tactic 'assumption' failed
--termination_by _ => fs2.size != 0 -> (HashMap.ofList fs2.toList.tail!).size < fs2.size
--termination_by _ => fs2.size != 0 -> f2tail.size < fs2.size -- unknown identifier 'f2tail.size'
termination_by _ => fs2.size != 0 -> f2tailsize < fs2.size -- unknown identifier 'f2tailsize'
I tried to minic the pattern used in reverse
without success.
On the other hand, I managed to make this variant work:
def applyMulAux (fs1: DCalcFactors) (l2: List DCalcFactor): DCalcFactors :=
match l2 with
| List.nil =>
fs1
| List.cons f2 t2 =>
match fs1.getOp f2.fst with
| some (f1a : Lean.Rat) =>
let f12 : DCalcFactor := ⟨ f2.fst, f1a + f2.snd ⟩
let fs1a := fs1.erase f2.fst
let fs1b := if 0 == f12.snd.num then fs1a else fs1a.insert f12.fst f12.snd
applyMulAux fs1b t2
| none =>
let fs1b := fs1.insert f2.fst f2.snd
applyMulAux fs1b t2
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors :=
applyMulAux fs1 fs2.toList
I would like to know what would it take to make the original version w/ a HashMap work as a well-founded recursion.
- Nicolas.
Mario Carneiro (Jan 30 2022 at 20:44):
The syntax is a bit off in your first snippet. What I was suggesting was something like this:
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors :=
let l2 : List DCalcFactor := fs2.toList
let f2 := l2.head!
let f2tail := HashMap.ofList l2.tail!
if h : fs2.size = 0 then
fs1
else
have : f2tail.size < fs2.size :=
have : fs2.size ≠ 0 → (HashMap.ofList fs2.toList.tail!).size < fs2.size := sorry
this h
match fs1.getOp f2.fst with
| none =>
let fs1b := fs1.insert f2.fst f2.snd
applyMul fs1b f2tail -- tactic 'assumption' failed
| some (f1a : Lean.Rat) =>
let f12 : DCalcFactor := ⟨ f2.fst, f1a + f2.snd ⟩
let fs1a := fs1.erase f2.fst
let fs1b := if 0 == f12.snd.num then fs1a else fs1a.insert f12.fst f12.snd
applyMul fs1b f2tail
termination_by _ fs1 fs2 => fs2.size
Mario Carneiro (Jan 30 2022 at 20:44):
that is, fs2.size ≠ 0 → (HashMap.ofList fs2.toList.tail!).size < fs2.size
is what you need to prove to make this termination argument work
Mario Carneiro (Jan 30 2022 at 20:45):
This code is somewhat inefficient in repeatedly converting a hashmap into a list and back again on every iteration, so I would expect your second approach to be more efficient in addition to being much easier to prove termination
Mario Carneiro (Jan 30 2022 at 20:49):
By the way, I see you use if 0 == value
a lot. I guess you learned to use these "yoda conditionals" in java land, but in lean there really isn't any need to do this at all, because assignment doesn't use =
and isn't an expression anyway - there is no way you could write an assignment inside an if conditional even if you wanted to
Mario Carneiro (Jan 30 2022 at 20:51):
It's also useful when capturing hypotheses about if-statements to write them idiomatically for the lemmas they will be used with. I changed the first if statement to if h : fs2.size = 0
so that the else branch gets an assumption fs2.size ≠ 0
, which is necessary for the proof of the sorry
Mario Carneiro (Jan 30 2022 at 20:52):
@Nicolas Rouquette
Ruben Van de Velde (Jan 30 2022 at 21:10):
You should only use yoda comparisons like 0 < value
:)
Patrick Massot (Jan 30 2022 at 21:16):
We should rename linter.ge_or_gt to linter.yoda
.
Nicolas Rouquette (Jan 31 2022 at 00:10):
Mario Carneiro said:
The syntax is a bit off in your first snippet. What I was suggesting was something like this:
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors := ... termination_by _ fs1 fs2 => fs2.size
Hum, based on what I see in mathlib4, I get the feeling that there are at least two ways to prove properties of functions (recursive or not):
1) Add the termination_by _ <arguments> => ....
clause
2) Use an auxiliary function where the return carries a proof
def applyMulAux (fs1: DCalcFactors) (fs2: DCalcFactors) : { fs2': DCalcFactors // -- invariant property about fs2 and fs2' } :=
...
def applyMul (fs1: DCalcFactors) (fs2: DCalcFactors) : DCalcFactors :=
applyMulAux fs1 fs2
If this is more or less correct, is this strategy explained in the lean4 doc somewhere?
Nicolas Rouquette (Jan 31 2022 at 00:41):
Mario Carneiro said:
For (2) your best bet is to put the functions in mathlib4. Most of us also have a
for_mathlib
file or directory in our projects containing definitions and lemmas that aren't really project specific and really ought to be PR'd to mathlib but we haven't got around to it yet
Interesting, where can I find an example of a for_mathlib
folder?
I understand that proving properties of functions depends a great deal on the proofs already available about the data structures involved in such functions. In my case, HashMap
seems very thin w.r.t proofs in lean4 compared to has_map
in Lean3's mathlib.
With Lean4, I am exploring using BinaryHeap
; however, I probably will need some extensions like contains
.
I would like to follow your suggestion and test such extensions in my application before suggesting a PR.
- Nicolas.
Mario Carneiro (Jan 31 2022 at 01:53):
The two mechanisms you mention are not mutually exclusive. You often need (2) in order to actually do the proof of (1)
Mario Carneiro (Jan 31 2022 at 01:58):
In this case you don't need it, but conceivably the way you recurse depends on the value you get back, in which case you have to know some property of the value. For example in heapifyDown
we need to know that the returned heap has the same size as the original so that subsequent accesses to the heap don't go out of bounds
Mario Carneiro (Jan 31 2022 at 02:01):
I understand that proving properties of functions depends a great deal on the proofs already available about the data structures involved in such functions. In my case, HashMap seems very thin w.r.t proofs in lean4 compared to has_map in Lean3's mathlib.
Yes, I started looking into this and it is more than a few hours of work to add all the missing lemmas for hashmaps, so I decided not to get into it today. Some things might not even be provable, because of the use of constant
s like UInt32.toUSize
in the implementation
Mario Carneiro (Jan 31 2022 at 02:03):
With Lean4, I am exploring using BinaryHeap; however, I probably will need some extensions like contains.
I would like to follow your suggestion and test such extensions in my application before suggesting a PR.
Contains? Binary heaps don't really support membership queries faster than linear search. If you want to do linear search you can do so by using heap.1
to access the underlying array
Kevin Buzzard (Jan 31 2022 at 07:40):
https://github.com/leanprover-community/lean-perfectoid-spaces/tree/master/src/for_mathlib is a for_mathlib directory FWIW
Nicolas Rouquette (Feb 01 2022 at 03:49):
Kevin Buzzard said:
https://github.com/leanprover-community/lean-perfectoid-spaces/tree/master/src/for_mathlib is a for_mathlib directory FWIW
Ah yes, though this is based on the lean3 code organization. For lean4, I am looking for lake-based configurations; I think I found two interesting examples for potential additions to mathlib4 and to lean4's library respectively:
https://github.com/lecopivo/SciLean/tree/master/SciLean/Mathlib
https://github.com/gebner/quote4/tree/master/Qq/ForLean
Nicolas Rouquette (Feb 01 2022 at 05:18):
Mario Carneiro said:
With Lean4, I am exploring using BinaryHeap; however, I probably will need some extensions like contains.
I would like to follow your suggestion and test such extensions in my application before suggesting a PR.Contains? Binary heaps don't really support membership queries faster than linear search. If you want to do linear search you can do so by using
heap.1
to access the underlying array
Right, I think you meant something like this:
import Mathlib.Data.BinaryHeap
namespace BinaryHeap
def contains {lt} [BEq α] (self : BinaryHeap α lt) (x : α) : Bool :=
self.arr.contains x
end BinaryHeap
Mario Carneiro (Feb 01 2022 at 08:47):
Right. (FYI you can use self.1
in place of self.arr
there, which is what I was alluding to)
Nicolas Rouquette (Feb 02 2022 at 05:27):
Mario Carneiro said:
Right. (FYI you can use
self.1
in place ofself.arr
there, which is what I was alluding to)
Wow.. I did not realize that lean4 supports an index-based reference for class/structure fields!
I think this should be explained in the doc (https://leanprover.github.io/lean4/doc/struct.html)
Separately, can you explain how useful this trick has been to your lean4 programming practice?
Mario Carneiro (Feb 02 2022 at 05:43):
I use it all the time in lean 3, it saves me from having to look up the names and it's shorter too
Mario Carneiro (Feb 02 2022 at 05:44):
Since BinaryHeap
is a newtype it's more natural to just use indexing notation since the field name is irrelevant
Last updated: Dec 20 2023 at 11:08 UTC