Zulip Chat Archive
Stream: lean4
Topic: Proving findLeast: an experience report
Mario Carneiro (Apr 10 2021 at 04:10):
This originated in a question by Jeremy Avigad about how to prove that this function:
def findLeast (a : Array Nat) : Nat := do
let mut smallest := a[0]
for i in [1:a.size] do
if a[i] ≤ smallest then
smallest := a[i]
return smallest
#eval findLeast #[8, 3, 10, 4, 6]
actually computes the least element of the array. The complete proof is here: https://gist.github.com/digama0/404b4a49ccf378847b7ad2c12374bae2
My experience report: There are still huge missing pieces and a lot of things are significantly more painful compared to lean 3. (Warning: this will come off a bit harsh, but I've tried to turn the unfocused frustration into as actionable items as I could.)
- The
trustme
tactic is a hack to work around the lack of equation lemmas. It acts likeexact
except it doesn't check that the given term has the correct type; it's safe because the kernel will catch you if you do something wrong, but it lets us implementdelta
in this case, to proveloop_eq
, which would beby delta loop
in lean 3.- This relates to https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/Smart.20unfolding.20produces.20ununfoldable.20terms . As far as I know, there is no way in lean 4 currently to prove the theorem without this "cheating" tactic.
- Stating the type of
loop_eq
is a challenge. Taking the result of#print loop
directly doesn't work for a number of type inference reasons. It seems that the delaborator still needs some more work. - I originally tried to prove
findLeast_min
with a single large theorem, but local let was not as powerful as I first thought, so I eventually abandoned it and this version uses a definitionfindLeastRec
for the inner let instead.- More specifically, I found that using local definitions with
theorem foo := ... where bar := ...
, the definition ofbar
is not available infoo
, it only appears as a local hypothesis, likehave bar := ...
which is no good for a definition likefindLeastRec
.
- More specifically, I found that using local definitions with
cases
and thematch
tactic seem to do approximately the same thing, but they are each limited in different ways. Thematch
tactic inherits from thematch
term the undesirable behavior that it does not generalize other variables in the context dependent on the one being pattern matched, unlikecases
- for example ifh : x < y
is in the context,match x with | 0 => ...
, you lose the fact that0 < y
. On the other hand,cases
apparently does not support nested pattern matches or the ability to use other expressions in the branches, for example matching aNat
as0
andn+1
instead ofzero
andsucc n
(and putting those terms in the goal).-
Keeping the tactic proof well formed is a challenge, and sometimes the whitespace sensitivity causes surprises. I particularly miss being able to use
{ }
to create blocks; lean 4{}
blocks don't work well with the indentation rules.-
More specifically:
have foo : type { tac tac }
doesn't work like it would in lean 3, the indentation level of the two tactics is different and this messes things up.
-
have : type
doesn't work as a tactic withoutfrom
orby
afterward. have := term
doesn't work. I use this all the time in lean 3 andhave _ from term
is not a great substitute. This is nothing that a mathlib prelude can't solve, though. (Can we just eliminatefrom
here in the syntax? I'd rather it just always use:=
.)- Implicit lambdas caused a number of issues because they get eagerly applied everywhere and it's difficult to control this behavior. I ended up having to avoid them entirely in things like the induction hypothesis. We need semi-implicits, I think.
- I should find a more specific example here, but implicit lambda introduction was something I had to force-disable more often than use.
- There are lots and lots of inaccessible names, which makes quick and dirty proving much more onerous. I generally write proofs in quick and dirty mode first and then clean them up once the proof is complete, because iteration on a dirty proof is a lot faster. Lean 4 makes you be a lot more explicit in each step which makes this harder.
- Writing tactics and finding things is very difficult. In lean 3, even without anything imported the entire tactic framework is available in
init
and everything is in namespacetactic
. In lean 4 there are a ton of different files and all the prominent constants likeCoreM
,ElabTermM
,elabTerm
are in different namespaces and require different files to be imported, and I had to go spelunking through the lean 4 source to find where anything is. - There are no tactic writing docs in the lean 4 reference, almost no tactic writing examples in tests, and looking through the lean 4 source for examples is of limited utility when many of the tools used inside lean 4 are of the
builtin
variety and you the user are supposed to be using something else.
-
Yury G. Kudryashov (Apr 10 2021 at 06:33):
About have _ := _
vs have _, from _
in lean3 (sorry for semi-offtopic). When I write have a = 5, from h _
, it is guaranteed to fail unless Lean can find _
. With have a = 5 := h _
it creates extra goals.
Mario Carneiro (Apr 10 2021 at 08:44):
Actually, that brings me to another point of note: refine
in lean 4 acts basically like lean 3 exact
, in that any _
get red squiggles on them and the tactic does not proceed. In order to recover lean 3 refine
you have to use ?_
instead. At the same time exact
also does that, except it has a much worse error reporting - any error and the whole exact
line gets highlighted. As a result, it seems like it might be better to just ditch exact
and make it a synonym for refine
, since it supports both modes explicitly.
Daniel Selsam (Apr 12 2021 at 16:37):
More specifically, I found that using local definitions with theorem foo := ... where bar := ..., the definition of bar is not available in foo, it only appears as a local hypothesis, like have bar := ... which is no good for a definition like findLeastRec.
FYI
theorem lookAtLetDef (n : Nat) : foo n = n + 1 :=
let bar (n : Nat) : Nat := n + 1
have foo n = bar n := rfl -- ok
sorry
theorem lookAtWhereDef (n : Nat) : foo n = n + 1 :=
have foo n = bar n := rfl -- type error
sorry
where bar (n : Nat) : Nat := n+1
theorem lookAtLetRecDef (n : Nat) : foo n = n + 1 :=
let rec bar (n : Nat) : Nat := n + 1
have foo n = bar n := rfl -- type error
sorry
Daniel Selsam (Apr 12 2021 at 16:48):
def foo (n : Nat) : Nat :=
let lbar (n : Nat) : Nat := n + 1
let rec lrbar (n : Nat) : Nat := n + 1
lbar n + lrbar n + wbar n where wbar n := n + 1
#print foo
/-
def foo : Nat → Nat :=
fun (n : Nat) =>
(fun (wbar : Nat → Nat) =>
let lbar : Nat → Nat := fun (n : Nat) => n + 1;
(fun (lrbar : Nat → Nat) => lbar n + lrbar n + wbar n) foo.lrbar)
foo.wbar
-/
So both let rec
and where
use auxiliary definitions, and my guess is that there is good reason to treat them as opaque while elaborating foo
. But maybe where
could notice when it is not recursive and be elaborated as a regular let
.
Mario Carneiro (Apr 12 2021 at 16:54):
In my case, I wanted the let rec
to turn into an auxiliary definition, not a local constant
Daniel Selsam (Apr 12 2021 at 16:54):
It seems that the delaborator still needs some more work.
The delaborator definitely needs more work (https://github.com/leanprover/lean4/issues/368) but it should be rare to need to elaborate brecOn
s once equation lemmas are being generated.
Mario Carneiro (Apr 12 2021 at 16:56):
specifically, I would like the delaborator to always produce a term that typechecks. For anything with a higher order matching problem it should make the motive := ...
explicit
Daniel Selsam (Apr 12 2021 at 16:58):
That is indeed the stated goal in the issue I linked to above. "round trip" means it re-elaborates to the original term.
Daniel Selsam (Apr 12 2021 at 17:00):
Mario Carneiro said:
In my case, I wanted the
let rec
to turn into an auxiliary definition, not a local constant
In your case, the function wasn't recursive.
theorem foo : False :=
let findLeastRec (f : Nat → Nat) (lo hi z j b : Nat) : Nat :=
Std.Range.forIn.loop (m := Id) [lo:hi]
(fun i r => if f i ≤ r then yield (f i) else yield r) z j b
sorry
Mario Carneiro (Apr 12 2021 at 17:01):
Oh I guess that's true. I think it was in another version of the proof
Daniel Selsam (Apr 12 2021 at 17:03):
In my case, I wanted the let rec to turn into an auxiliary definition, not a local constant
I think the issue is that it could be mutually recursive with the top-level definition. So similar to my suggestion that where
detect when it is not recursive, perhaps letrec
and where
could also detect when it is "self-contained recursive", i.e. safe to turn into an auxiliary definition that does not refer to the current one.
Mario Carneiro (Apr 12 2021 at 17:06):
Oh, that's interesting. How commonly is that used? Maybe there should be a special indicator for that, like where mutual
or something
Mario Carneiro (Apr 12 2021 at 17:08):
detecting self-contained recursive is tough because you have to first run the tactics to find out what the term is, and the data given to the tactics depends on the result of the analysis (since they would be able to see whether an auxiliary definition was created)
Daniel Selsam (Apr 12 2021 at 17:13):
Currently, both of these work:
mutual
partial def mfoo (n : Nat) : Nat := if n = 0 then 0 else mbar (n-1)
partial def mbar (n : Nat) : Nat := if n = 0 then 1 else mfoo (n-1)
end
partial def foo (n : Nat) : Nat := if n = 0 then 0 else wbar (n-1) where
wbar (n : Nat) : Nat := if n = 0 then 1 else foo (n-1)
Daniel Selsam (Apr 12 2021 at 17:19):
I think where mutual
is tricky because you might batch many different functions in a where
clause. But what about function-specific prefix?
partial def foo (n : Nat) : Nat := if n = 0 then base else wbar (n-1) where
mutual wbar (n : Nat) : Nat := if n = 0 then 1 else foo (n-1)
base : Nat := 0
Mario Carneiro (Apr 12 2021 at 17:24):
I was thinking you would either make all of them mutual or none
Mario Carneiro (Apr 12 2021 at 17:26):
but your version reads well
Mario Carneiro (Apr 12 2021 at 17:28):
if you could put def
or let
or have
on definitions in where clauses that would also let you control whether they show up as local constants, auxiliary definitions, or let bindings
Daniel Selsam (Apr 12 2021 at 17:29):
for example if h : x < y is in the context, match x with | 0 => ..., you lose the fact that 0 < y.
And presumably you think it is too annoying to match on h
explicitly as in:
theorem foo (n : Nat) (h : n < n + 1) : n < n + 1 :=
match n, h with
| 0, h2 => h2
| n+1, h2 => h2
I think it is good practice to match on the h
explicitly, but agree it is annoying to need to put it in every branch. What about a macro:
theorem foo (n : Nat) (h : n < n + 1) : n < n + 1 :=
match n generalizing h with
| 0 => ...
| n+1 => ...
-- could turn into:
theorem foo (n : Nat) (h : n < n + 1) : n < n + 1 :=
match n, h with
| 0, h => ...
| n+1, h => ...
Mario Carneiro (Apr 12 2021 at 17:35):
In cases
, you just use cases n
and it generalizes everything in the context that depends on n
. I've never seen a situation where this was undesirable
Mario Carneiro (Apr 12 2021 at 17:36):
induction
is another matter, but match
doesn't support induction anyway
Mario Carneiro (Apr 12 2021 at 17:37):
Note that induction
will automatically add generalizing h
whenever h
depends on the induction variable
Mario Carneiro (Apr 12 2021 at 17:38):
so you actually have to use clear
if you don't want that
Daniel Selsam (Apr 12 2021 at 18:16):
Mario Carneiro said:
if you could put
def
orlet
orhave
on definitions in where clauses that would also let you control whether they show up as local constants, auxiliary definitions, or let bindings
I tentatively like this proposal
Daniel Selsam (Apr 12 2021 at 18:21):
Mario Carneiro said:
so you actually have to use
clear
if you don't want that
Seems like apples-to-oranges since match
is term mode, not tactic mode.
On the other hand, cases apparently does not support nested pattern matches or the ability to use other expressions in the branches, for example matching a Nat as 0 and n+1 instead of zero and succ n (and putting those terms in the goal).
Can you write the syntax you would want for cases
and induction
?
Daniel Selsam (Apr 12 2021 at 18:28):
Writing tactics and finding things is very difficult.
FYI here is the API for builtin tactics designed for interactive mode: https://github.com/leanprover/lean4/blob/master/src/Init/Notation.lean#L189-L325
Daniel Selsam (Apr 12 2021 at 18:31):
Writing tactics and finding things is very difficult.
I agree, though one doesn't need to find nearly as much when there are working, illustrative examples to copy-paste-tweak. How about we start collecting a set of informative examples in mathlib-prelude?
Yakov Pechersky (Apr 12 2021 at 18:34):
Here's an example of a macro that took me very long to figure out. I'm sure it's still buggy. How does one use lemma
instead of theorem
?
macro "lemma" n:declId sig:declSig ":=" s:term : command =>
`(theorem $n:declId $sig:declSig := $s:term)
Daniel Selsam (Apr 12 2021 at 18:37):
Also, there are a few tricks we can put in a doc, e.g. to find how a command (e.g. namespace
) is elaborated,
$ grep -rni "commandElab.*namespace" ../../lean4/src/Lean
../../lean4/src/Lean/Elab/Command.lean:376:@[builtinCommandElab «namespace»] def elabNamespace : CommandElab := fun stx =>
or parsed:
$ grep -rni "commandParser.*namespace" ../../lean4/src/Lean/
../../lean4/src/Lean/Parser/Command.lean:67:@[builtinCommandParser] def «namespace» := leading_parser "namespace " >> ident
Mario Carneiro (Apr 12 2021 at 18:45):
Daniel Selsam said:
Mario Carneiro said:
so you actually have to use
clear
if you don't want thatSeems like apples-to-oranges since
match
is term mode, not tactic mode.
My original comment was about the match
tactic as compared to the cases
tactic
Mario Carneiro (Apr 12 2021 at 18:48):
Daniel Selsam said:
On the other hand, cases apparently does not support nested pattern matches or the ability to use other expressions in the branches, for example matching a Nat as 0 and n+1 instead of zero and succ n (and putting those terms in the goal).
Can you write the syntax you would want for
cases
andinduction
?
here's a start:
cases n
| 0 => ...
| n+1 => ...
induction
is harder since it has the extra ih
argument hanging off.
induction n
| 0 => ...
| n+1, ih => ...
Daniel Selsam (Apr 12 2021 at 18:54):
Yakov Pechersky said:
Here's an example of a macro that took me very long to figure out. I'm sure it's still buggy. How does one use
lemma
instead oftheorem
?macro "lemma" n:declId sig:declSig ":=" s:term : command => `(theorem $n:declId $sig:declSig := $s:term)
The only issue I see with yours is that theorem
is actually a bit more powerful than that: https://github.com/leanprover/lean4/blob/master/src/Lean/Parser/Command.lean#L40 After adding a line to that file to export the declVal
shorthand, you can write:
macro "lemma" n:declId sig:declSig val:declVal : command => `(theorem $n $sig $val)
I believe this will make lemma
and theorem
equivalent.
Mario Carneiro (Apr 12 2021 at 18:56):
Yakov Pechersky said:
Here's an example of a macro that took me very long to figure out. I'm sure it's still buggy. How does one use
lemma
instead oftheorem
?macro "lemma" n:declId sig:declSig ":=" s:term : command => `(theorem $n:declId $sig:declSig := $s:term)
Based on the theorem
parser, I think you want the last bit to be declVal
:
macro "lemma" n:declId sig:declSig val:declVal : command =>
`(theorem $n:declId $sig:declSig $val:declVal)
However, this causes the same quotation panic that I've triggered in two other threads
Yakov Pechersky (Apr 12 2021 at 18:57):
Right, so the working one I have is weaker, but functional. I didn't file a bug report on the panic, rather tried to find a stopgap partial replacement.
Mario Carneiro (Apr 12 2021 at 18:57):
After adding a line to that file to export the declVal shorthand,
Oh, that sounds problematic
Daniel Selsam (Apr 12 2021 at 18:58):
Mario Carneiro said:
After adding a line to that file to export the declVal shorthand, you can write:
Oh, that sounds problematic
I will PR it.
Mario Carneiro (Apr 12 2021 at 18:59):
This actually relates to another issue I've hit on: there are a lot of private
functions in lean 4. I had to kill these with fire in lean 3 because they really hamper reuse
Daniel Selsam (Apr 12 2021 at 19:00):
Mario Carneiro said:
This actually relates to another issue I've hit on: there are a lot of
private
functions in lean 4. I had to kill these with fire in lean 3 because they really hamper reuse
Can you please explain the issue?
Mario Carneiro (Apr 12 2021 at 19:00):
I recently tried to get the parsed PreDeclaration
for a theorem. In order to do so, I had to bring in 15 or so (large) private definitions, before eventually getting stuck on a private macro, which I have no idea how to duplicate
Mario Carneiro (Apr 12 2021 at 19:00):
You can't call private functions
Mario Carneiro (Apr 12 2021 at 19:01):
so that means if you want to adapt a function that calls a private function, you have to copy and paste it, as well as any private functions it calls and so on transitively
Mario Carneiro (Apr 12 2021 at 19:01):
it's a huge pain
Mario Carneiro (Apr 12 2021 at 19:02):
I think private
should never be used. You never know when someone is going to want to use your function
Daniel Selsam (Apr 12 2021 at 19:02):
Is this just for debugging?
Mario Carneiro (Apr 12 2021 at 19:03):
Yes and no
Daniel Selsam (Apr 12 2021 at 19:03):
Mario Carneiro said:
I think
private
should never be used. You never know when someone is going to want to use your function
This seems pretty extreme. Encapsulation is generally considered good design.
Mario Carneiro (Apr 12 2021 at 19:04):
In that particular example it was for debugging, but it's totally legitimate to want to work with PreDefinition
or any other implementation item
Mario Carneiro (Apr 12 2021 at 19:04):
Not in a theorem prover that is meant to be hacked on by users
Daniel Selsam (Apr 12 2021 at 19:04):
Mario Carneiro said:
so that means if you want to adapt a function that calls a private function, you have to copy and paste it, as well as any private functions it calls and so on transitively
Personally, I tweak lean4 and rebuild all the time for things like this. The slow recompilation speed is annoying but my own fault for not having a threadripper.
Mario Carneiro (Apr 12 2021 at 19:04):
Yeah, I'm worried I'm going to need to fork lean 4
Mario Carneiro (Apr 12 2021 at 19:05):
Ultimately I thought the idea was that we don't need to
Mario Carneiro (Apr 12 2021 at 19:08):
Daniel Selsam said:
Mario Carneiro said:
I think
private
should never be used. You never know when someone is going to want to use your functionThis seems pretty extreme. Encapsulation is generally considered good design.
If there was a way to open a namespace and get at the private functions this would be fine. But lean is intentionally designed to be modified and I don't think there is a single component of the system that isn't "incomplete" in the sense that users may want to add functionality to what is already there, and work with the data structures of that file
Mario Carneiro (Apr 12 2021 at 19:11):
Certainly in mathlib every single time someone thought it would be a good idea to put private
it turned out to be a mistake. At minimum, it is difficult to prove theorems about a private function, so in the far future when someone decides to prove e.g. the parser correct, they will get stuck on any "encapsulation"
Daniel Selsam (Apr 12 2021 at 19:12):
Mario Carneiro said:
Yeah, I'm worried I'm going to need to fork lean 4
FWIW I don't think it would be so bad for there to be a lean-community:lean4 as long as the patches were small enough that it could be frequently rebased.
Mario Carneiro (Apr 12 2021 at 19:12):
I can live with that if others can
Mario Carneiro (Apr 12 2021 at 19:13):
At least then I can get the mountain of PRs I'm holding off on off my back
Daniel Selsam (Apr 12 2021 at 19:16):
Even if the goal is to keep the lean-community version exactly equal to the main one, it may still make sense to fork now and experiment with your suggested changes. Leo will be much more likely to consider a PR down the road if it has been stress-tested and if there is community consensus on its value.
Mario Carneiro (Apr 12 2021 at 19:17):
(okay, not a mountain, a few pebbles. But I don't know how to build the mountain when I can't place the first pebble.)
Mario Carneiro (Apr 12 2021 at 19:18):
Can someone who knows how to do so set up a community lean 4 nightly?
Daniel Selsam (Apr 12 2021 at 20:37):
Mario Carneiro said:
- Keeping the tactic proof well formed is a challenge, and sometimes the whitespace sensitivity causes surprises.
Note that you can also use braces and semicolons:
theorem foo (n : Nat) : True := by {
match n with
| 0 => _
| n+1 => _; exact sorry;
exact sorry
}
Mario Carneiro (Apr 12 2021 at 20:42):
not quite:
theorem foo (n : Nat) : True := by {
match n with
| 0 => _
| n+1 => _; exact sorry;
exact sorry
}
Daniel Selsam (Apr 12 2021 at 20:44):
Ah, it looks like all subsequent bars after the first need to have at least as much offset, even in tactic-mode-with-bars
Mario Carneiro (Apr 12 2021 at 20:44):
I haven't found a whitespace insensitive version of the syntax either
Mario Carneiro (Apr 12 2021 at 20:46):
Oh, also it seems like _
is a tactic now? I have a macro in my test file for that
Mario Carneiro (Apr 12 2021 at 20:46):
I used refine _
as the expansion because that gives a red squiggle and tells you the type, but also eats a goal and allows the rest to proceed, like sorry
Sebastian Ullrich (Apr 12 2021 at 20:48):
This is also the reason why
have foo : type
{ tac
tac }
didn't work: inside braces, you must use semicolons. I've written a fair chunk of tactic proofs in Lean 4, but I never noticed that one because I tend to use Lean 4's new structuring techniques (all the stuff with =>
) instead of braces.
Daniel Selsam (Apr 12 2021 at 20:49):
Mario Carneiro said:
I used
refine _
as the expansion because that gives a red squiggle and tells you the type, but also eats a goal and allows the rest to proceed, likesorry
The match
tactic calls refine
automatically (https://github.com/leanprover/lean4/blob/master/src/Lean/Elab/Tactic/Match.lean#L66) and has for 6 months according to GitHub's blame
Mario Carneiro (Apr 12 2021 at 20:49):
I'm fine with using the new structuring techniques, but it's awkward around have
because of the by
Mario Carneiro (Apr 12 2021 at 20:49):
I mean the _
tactic, as in by _
Mario Carneiro (Apr 12 2021 at 20:49):
that did not work a few weeks ago
Sebastian Ullrich (Apr 12 2021 at 20:49):
But indentation should work for that, right?
have t by
t'1
t'2
t
Mario Carneiro (Apr 12 2021 at 20:49):
It does
Mario Carneiro (Apr 12 2021 at 20:50):
I just want to drop the by
Mario Carneiro (Apr 12 2021 at 20:50):
lean 3 doesn't need it
Daniel Selsam (Apr 12 2021 at 20:51):
Mario Carneiro said:
I mean the
_
tactic, as inby _
theorem foo : True := by _
gives me:
error: expected '{' or tactic
error: unexpected command
Mario Carneiro (Apr 12 2021 at 20:51):
Also, a common lean 3 proof style is
have := lemma,
rw [lemma2] at this,
exact this
This doesn't work in lean 4 because the have
doesn't proceed until all the metavariables in lemma
are filled (while the proof technique might be using lemma2
and the exact
to figure out the metavariables)
Mario Carneiro (Apr 12 2021 at 20:52):
@Daniel Selsam That's odd, since your badly formatted code example uses the _
tactic twice
Daniel Selsam (Apr 12 2021 at 20:52):
No, because the match
tactic is transforming the syntax to call refine
Mario Carneiro (Apr 12 2021 at 20:53):
you what now?
Mario Carneiro (Apr 12 2021 at 20:53):
how does that work?
Daniel Selsam (Apr 12 2021 at 20:54):
This is the link I posted a few comments above: https://github.com/leanprover/lean4/blob/master/src/Lean/Elab/Tactic/Match.lean#L66-L68
Daniel Selsam (Apr 12 2021 at 20:55):
Sebastian Ullrich said:
But indentation should work for that, right?
have t by t'1 t'2 t
FWIW I like the by
here.
Mario Carneiro (Apr 12 2021 at 20:56):
Does the by
create a new tactic scope like in lean 3?
Mario Carneiro (Apr 12 2021 at 20:57):
if so that might explain the reason why the rwa
proof style fails
Mario Carneiro (Apr 12 2021 at 21:00):
Thoughts on =>
as alternative / in addition to by
?
Mario Carneiro (Apr 12 2021 at 21:00):
and :=
as alternative to from
as I mentioned before
Mario Carneiro (Apr 12 2021 at 21:03):
I'm not a fan of the many syntaxes for introducing subgoals, by
to start, =>
after cases, match, induction, ;
after refine (actually I haven't figured out how to select refine subgoals using multiple blocks yet)
Mario Carneiro (Apr 12 2021 at 21:04):
refine foo ?_ ?_ ?_
tac1a
tac1b
done
tac2a
tac2b
done
tac3a
tac3b
done
Mario Carneiro (Apr 12 2021 at 21:05):
With lean 3 style blocks this would be
refine foo ?_ ?_ ?_
{ tac1a
tac1b }
{ tac2a
tac2b }
{ tac3a
tac3b }
all the lean 4 options seem to be a lot more verbose
Mario Carneiro (Apr 12 2021 at 21:08):
Sebastian Ullrich said:
This is also the reason why
have foo : type { tac tac }
didn't work: inside braces, you must use semicolons.
Can we make that work with whitespace sensitivity? The rule I want is that the column starts at the first token after the {
Sebastian Ullrich (Apr 12 2021 at 21:08):
They look both pretty bad to me. How about naming them and using case
?
Mario Carneiro (Apr 12 2021 at 21:09):
because they don't have names
Daniel Selsam (Apr 12 2021 at 21:09):
@Mario Carneiro This works for me:
structure A : Prop where
structure B : Prop where
structure C : Prop where
axiom AB : A → B
axiom BC : B → C
theorem foo (a : A) : C ∧ C := by
refine And.intro ?x ?y
{ apply BC; apply AB; assumption }
{ apply BC; apply AB; assumption }
Mario Carneiro (Apr 12 2021 at 21:09):
Yes that works, but it means I have to give up whitespace sensitive lines
Mario Carneiro (Apr 12 2021 at 21:10):
which screws up the overall formatting if I'm using it elsewhere
Mario Carneiro (Apr 12 2021 at 21:10):
if some tactic lines end in ;
and others don't that will read weirdly
Daniel Selsam (Apr 12 2021 at 21:11):
This also works:
theorem foo (a : A) : C ∧ C := by
refine And.intro ?x ?y
focus
apply BC
apply AB
assumption
focus
apply BC
apply AB
assumption
Mario Carneiro (Apr 12 2021 at 21:11):
oh that's not bad
Daniel Selsam (Apr 12 2021 at 21:11):
And there can probably be a variant where you name the focus branches
Mario Carneiro (Apr 12 2021 at 21:11):
that's the version sebastian was suggesting
Daniel Selsam (Apr 12 2021 at 21:11):
like case ?x
instead of focus
Mario Carneiro (Apr 12 2021 at 21:12):
but I don't want to give names to goals where the best name is an ordinal
Mario Carneiro (Apr 12 2021 at 21:13):
Also, can ?
work instead of ?_
? It's an extra character compared to lean 3
Sebastian Ullrich (Apr 12 2021 at 21:14):
You can use refine' foo _ _ _
:)
Daniel Selsam (Apr 12 2021 at 21:14):
I am pro-names in general
Mario Carneiro (Apr 12 2021 at 21:14):
I want to be able to keep the structure of lean 3 proofs for the most part
Mario Carneiro (Apr 12 2021 at 21:15):
names should be an option
Daniel Selsam (Apr 12 2021 at 21:15):
meaning the same lack of structure?
Mario Carneiro (Apr 12 2021 at 21:15):
They are structured
Mario Carneiro (Apr 12 2021 at 21:15):
they are block structured proofs
Mario Carneiro (Apr 12 2021 at 21:15):
We are pretty strict about that
Mario Carneiro (Apr 12 2021 at 21:16):
I'm not about to rewrite all lean 3 proofs for style in the middle of a giant port
Mario Carneiro (Apr 12 2021 at 21:19):
You can write unstructured proofs in lean 3 but I'm not talking about those
Daniel Selsam (Apr 12 2021 at 21:19):
Got it
Mario Carneiro (Apr 12 2021 at 21:20):
in fact it seems like lean 4's support for unstructured proofs is pretty much the same
Daniel Selsam (Apr 12 2021 at 21:21):
It does seem so. I thought at some point the plan was to require names and case ?x
but I guess that was ditched.
Mario Carneiro (Apr 12 2021 at 21:21):
I like the current setup: you can write names and case if you want but you don't have to
Mario Carneiro (Apr 12 2021 at 21:22):
lean 3 also cultivated (at least) two distinct styles: mathlib's block structured proofs and NNG's unstructured proofs (plus term mode proofs, which have several different styles)
Mario Carneiro (Apr 12 2021 at 21:23):
I can imagine having a style linter to ensure adherence to one style or another
Mario Carneiro (Apr 12 2021 at 21:23):
but the core syntax should be multi-paradigm
Sebastian Ullrich (Apr 12 2021 at 21:25):
Mario Carneiro said:
I'm not about to rewrite all lean 3 proofs for style in the middle of a giant port
Then that should be discussed in a different topic. I don't mind if mathport implements its own tactic compatibility layer. But this topic started as an experience report on Lean 4, not porting, so it should discuss the best solutions for Lean 4 irrespective of Lean 3 solutions. Which always was our modus operandi for Lean 4.
Mario Carneiro (Apr 12 2021 at 21:27):
Fair enough. I guess I'm saying that even for green field lean 4 projects I'm not sold on the | =>
stuff
Mario Carneiro (Apr 12 2021 at 21:27):
in coq I think the equivalent is -
Mario Carneiro (Apr 12 2021 at 21:28):
I really want a way to not waste a line saying "here's a goal"
Daniel Selsam (Apr 12 2021 at 21:32):
@Sebastian Ullrich
I've written a fair chunk of tactic proofs in Lean 4
Are these on GitHub somewhere?
Sebastian Ullrich (Apr 12 2021 at 21:33):
Not publicly, unfortunately, since they are the solutions to our course project :frowning:
Sebastian Ullrich (Apr 12 2021 at 21:37):
Mario Carneiro said:
in coq I think the equivalent is
-
I kind of like that, I think it would be a better fit for the whitespace-sensitive syntax than braces
Yury G. Kudryashov (Apr 12 2021 at 22:23):
Daniel Selsam (Apr 12 2021 at 22:30):
Mario Carneiro said:
- Implicit lambdas caused a number of issues because they get eagerly applied everywhere and it's difficult to control this behavior. I ended up having to avoid them entirely in things like the induction hypothesis. We need semi-implicits, I think.
- I should find a more specific example here, but implicit lambda introduction was something I had to force-disable more often than use.
I am curious about this one. I don't see any @fun
s in the file you posted so I am guessing it was earlier in development.
Mario Carneiro (Apr 12 2021 at 22:41):
I was curious about when coq uses -
vs *
; It is discussed in the "bullets" section, and it seems like you can use any of -
, +
and *
to select subgoals, where I guess the idea is that you can use different delimiters at different levels. (I assume all delimiters at a given subgoal splitting have to agree.) Personally I think it would be better to just use one of them, since users might come up with some other meaning for the other sigils
Mario Carneiro (Apr 12 2021 at 22:49):
I can't replicate the @fun
issues. I think I was forgetting fun {i} =>
Mario Carneiro (Apr 12 2021 at 22:50):
Oh there was a bug in a tactic involving implicit lambdas, let me see if I can replicate
Mario Carneiro (Apr 12 2021 at 22:57):
example : ∀ {i : Nat}, i < 0 := by
-- ⊢ ∀ {i : Nat}, i < 0
have _ from 1
-- i✝ this : Nat ⊢ i✝ < 0
skip
The same thing happens with let
and match
Zygimantas Straznickas (Apr 12 2021 at 23:43):
Mario Carneiro said:
I was curious about when coq uses
-
vs*
; It is discussed in the "bullets" section, and it seems like you can use any of-
,+
and*
to select subgoals, where I guess the idea is that you can use different delimiters at different levels. (I assume all delimiters at a given subgoal splitting have to agree.) Personally I think it would be better to just use one of them, since users might come up with some other meaning for the other sigils
Yup, any bullet can be used in any nesting level as long as the same bullet is used for the same level. Also, indentation doesn't matter when using bullets -- these are all legal (though obviously only the first one is a good idea):
Theorem ex : (True /\ True /\ (True /\ True) /\ True).
Proof.
split; [|split]; [| |split]. (* split into multiple goals for demonstration*)
* constructor.
* constructor.
* split.
- constructor.
- constructor.
* constructor.
Qed.
Theorem ex2 : (True /\ True /\ (True /\ True) /\ True).
Proof.
split; [|split]; [| |split]. (* split into multiple goals for demonstration*)
* constructor.
* constructor.
* split.
- constructor. - constructor.
* constructor.
Qed.
Theorem ex3 : (True /\ True /\ (True /\ True) /\ True).
Proof.
split; [|split]; [| |split]. (* split into multiple goals for demonstration*)
* constructor. * constructor. * split. - constructor. - constructor. * constructor.
Qed.
One limitation of this notation is that you can do at most three levels of nesting. But one might argue that if you need more, your proof is too complicated and you need to refactor :)
Mario Carneiro (Apr 13 2021 at 00:08):
Oh I see, it's not whitespace sensitive so the different sigils are required
Daniel Selsam (Apr 13 2021 at 01:52):
Mario Carneiro said:
example : ∀ {i : Nat}, i < 0 := by -- ⊢ ∀ {i : Nat}, i < 0 have _ from 1 -- i✝ this : Nat ⊢ i✝ < 0 skip
The same thing happens with
let
andmatch
This is really weird. Here are some more examples:
example (P : Prop) : ∀ {p : P}, P := by
exact id -- error: type mismatch, expected type P
example (P : Prop) : ∀ {p : P}, P := by
exact @id _ -- works
example (P : Prop) : ∀ {p : P}, P := by
apply id -- works
example (P : Prop) : ∀ {p : P}, P := by
have _ from 1
apply id -- error: unsolved goals |- P
Mario Carneiro (Apr 13 2021 at 01:58):
yeah, I think that first example was the issue I hit that caused me to abandon the approach
Daniel Selsam (Apr 13 2021 at 01:58):
I don't even understand what is happening there.
Mario Carneiro (Apr 13 2021 at 01:59):
the type just felt very unstable, as if looking at it wrong causes it to get instantiated
Daniel Selsam (Apr 13 2021 at 02:55):
@Yakov Pechersky FYI declVal
is exposed now on master so
macro "lemma" n:declId sig:declSig val:declVal : command => `(theorem $n $sig $val)
should work.
Daniel Selsam (Apr 13 2021 at 03:10):
I just read over the implicit-lambda code. It is surprisingly aggressive but based on relatively simple rules. Basically, any variant of the above examples will have a lambda inserted, unless it (a) doesn't involve elaborating a term with the expected type or (b) explicitly disables it. The reason the apply id
example works is that apply
(unlike exact
) detects that id
is an identifier and doesn't actually elaborate it using the expected type.
Jeremy Avigad (Apr 13 2021 at 15:19):
Sorry to be late to this party, but regarding the "private" keyword, would it help to keep their names but prefix the namespace with "private"? They could then be filtered by documentation and search, but determined users could still access them. The main problem would be name clashes -- users would have to use private foo
and private foo1
and private foo2
in the namespace where they could just use private foo
repeatedly before.
Sebastian Ullrich (Apr 13 2021 at 15:38):
The names are preserved, including namespaces: https://github.com/leanprover/lean4/blob/bf4b9b0ccd7688d3d4024bc06a0c7a3b207124bd/src/Lean/Modifiers.lean#L24-L27
A dedicated meta hacker could write a tear_open_private
command that walks through the declarations of a specified module and creates accessible aliases of such names. But there would be no guarantee that this will keep working indefinitely, of course. For example, it is incompatible with any kind of module system where private declarations are not part of the "signature" .olean file to begin with (though the full .olean file should still be around to load instead). At that point private declarations would also become much more useful: you could add new ones without having to recompile (some) downstream modules.
Daniel Selsam (Apr 13 2021 at 15:43):
FWIW I am skeptical that private
is a real issue right now. For random experiments and traces, locally rebuilding Lean is fine.
Gabriel Ebner (Apr 13 2021 at 16:09):
tear_open_private
is called import_private
right now. :smile:
Daniel Selsam (Apr 13 2021 at 16:10):
(and if there are some private
defs that are deemed important to expose, these can be changed on case-by-case basis)
Gabriel Ebner (Apr 13 2021 at 16:11):
For example, it is incompatible with any kind of module system where private declarations are not part of the "signature" .olean file to begin with
I don't think that is realistic. Private definitions are part of the public interface because they are exposed during unification. In particular, changing private definitions is a breaking change because it affects definitional equality.
On the other hand, leaving out "definitions" of constants would be fair game.
Sebastian Ullrich (Apr 13 2021 at 16:19):
Ah, but the module system that only exists in my head would also seal (make opaque) public definitions by default... it might be mostly useful for programming and not proving, so it would most likely be opt-in and the standard import
would keep its current lax semantics.
Sebastian Ullrich (Apr 13 2021 at 16:21):
Using private declarations from a reducible
or otherwise exposed declaration should ideally be a compiler error
Gabriel Ebner (Apr 13 2021 at 16:22):
Would type-checking also be restricted to reducible transparency?
Sebastian Ullrich (Apr 13 2021 at 16:24):
For imported definitions? If you imported them via say import signature
and they don't have some other attribute that says "transparent in signature imports but not reducible
", then yes.
Sebastian Ullrich (Apr 13 2021 at 16:26):
So it's not really a change in the transparency predicate, but import signature
importing definitions as value-less constants by default.
Sebastian Ullrich (Apr 13 2021 at 16:29):
And then you'd just need some content-addressed build system to actually make use of that... it's as thought-out as it sounds, yes
Gabriel Ebner (Apr 13 2021 at 16:30):
Ah, so things like def Options := KVMap
would still reduce because they're not imported as signature. It's an interesting dialect, but probably not that useful if you want to keep theorems close to your definitions.
Last updated: Dec 20 2023 at 11:08 UTC