-
Notifications
You must be signed in to change notification settings - Fork 942
doc(elabissues): typeclass loop issue and suggestions #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /- | ||
| This example demonstrates a case where Lean4's tabled typeclass resolution may loop. | ||
| It also suggests a workaround, new instance binder semantics, new syntax support, and a new instance validation rule. | ||
| -/ | ||
|
|
||
| class Field (K : Type) := (u : Unit) | ||
| class VectorSpace (K : Type) [Field K] (E : Type) := (u : Unit) | ||
| instance VectorSpaceSelf (K : Type) [Field K] : VectorSpace K K := {u:=()} | ||
| class CompleteSpace (α : Type) := (u : Unit) | ||
| def AlgebraicClosure (K : Type) [Field K] : Type := K | ||
|
|
||
| /- | ||
| Note that this instance is not a problem when `K` is known, | ||
| because it will only ever "unpack" AlgebraicClosures, not create new ones | ||
| However, if `K` is ever not known, it will create them ad infinitum! | ||
| -/ | ||
| instance AlgebraicClosure.Field (K : Type) [Field K] : Field (AlgebraicClosure K) := {u:=()} | ||
|
|
||
| /- | ||
| Here is the "bad" instance one may be tempted to write: | ||
|
|
||
| << | ||
| instance bad (K E : Type) [Field K] [VectorSpace K E] [CompleteSpace K] : CompleteSpace E := {u := ()} | ||
| >> | ||
|
|
||
| It is bad because typeclass resolution will try to find `Field ?K` before it knows what `?K` is, | ||
| which in conjunction with the instance `AlgebraicClosure.Field`, will cause resolution to diverge. | ||
|
|
||
| Here is the workaround, which is very ugly: | ||
|
|
||
| << | ||
| instance veryUgly (K E : Type) {fK : Field K} [@VectorSpace K fK E] [CompleteSpace K] : CompleteSpace E := {u := ()} | ||
| >> | ||
|
|
||
| Here, `Field ?K` is not solved by typeclass resolution, and instead `VectorSpace ?K ?fK E` will be solved first instead. | ||
|
|
||
| With the Lean3 instance semantics, one could make this less ugly by writing | ||
| -/ | ||
| instance ugly (K E : Type) {_ : Field K} [VectorSpace K E] [CompleteSpace K] : CompleteSpace E := {u := ()} | ||
|
|
||
| /- | ||
| This would work because the `Field K` instance would still be considered for typeclass resolution even though it was not in a `[]` binder. | ||
| However, the original plan for Lean4 was that one would only consider `[]` binders for typeclass resolution. | ||
| We suggest that we revert to the Lean3 protocol instead, and consider any local variable with class type as a candidate instance. | ||
|
|
||
| Finally, this instance could be made reasonable by allowing `{}` binders without names: | ||
| << | ||
| instance reasonable (K E : Type) {Field K} [VectorSpace K E] [CompleteSpace K] : CompleteSpace E := {u := ()} -- should work in Lean4 | ||
| >> | ||
| -/ | ||
|
|
||
| axiom K : Type | ||
| instance K_Field : Field K := {u:=()} | ||
|
|
||
| #synth CompleteSpace K -- should fail quickly (and in particular, not run forever) | ||
|
|
||
| /- | ||
| The bad instance above could trigger a validation warning: | ||
|
|
||
| << | ||
| instance bad (K E : Type) [Field K] [VectorSpace K E] [CompleteSpace K] : CompleteSpace E := {u := ()} | ||
| >> | ||
| -- Warning: argument #3 is a class that occurs in downstream arguments and not the return type. | ||
| -- You may want to replace [Field K] with {Field K} so typeclass resolution infers this instance after solving downstream instances. | ||
| -/ | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syntax
{Field K}is ambiguous :(It can be interpreted also as
{Field K : _}. Note that we use this version all the time (e.g.,{α β}).@Kha: please take a look at this thread, and chime in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kha: @dselsam and I considered silently converting
into
since the local instance
Field Koccurs at[VectorSpace K E].We discarded it because we believed it would confuse users. However, I am considering it again.
Another motivation is the following example:
works as expected, and the argument
[Field K]is automatically added to the signature as expected.On the other hand, the following example will fail to be elaborated:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am on the fence. The "silent conversion" described above is changing the "logic program" written by the user. For this particular scenario, the modification is beneficial, but in other scenarios, it may have a negative impact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The silent conversion would have been my first idea as well, but I can see how it could not always be wanted. We should search for examples where it is not.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dselsam AFAICT, Coq does something quite similar to this by delaying dependent goals, right? So there is at least precedent. How would you compare these two approaches (delaying and conversion)?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kha Yes, it is a good point. During typeclass resolution, Coq will "shelve" dependent goals and only try them after all goals that depend on them have been solved.
It is also worth noting Coq's implicit generalization feature `{...}, which allows omitting the field argument from the instance declaration:
Though if you recall, you found this surprising when you exported the algebraic hierarchy to Coq. Also, if
Field Kappears multiple times, it would be implicitly generalized multiple times as well.Also note that every variant of this problem I try in Coq immediately enters an infinite loop for silly reasons, because Coq doesn't table.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kha Here is an artificial example where they differ:
Suppose, for some reason, the user made this
Base αargument implicit instead of inst-implicit:Then replacing
BaseAsInstImplicitwithBaseAsImplicitbelow will cause typeclass resolution to fail on the#synth Topquery below:On the other hand, Coq's approach of shelving goals would still work,
because it would shelve the
[Base α]ofBaseAsInstImplicit,but eventually return to it and find the
BaseKinstance.