opaque: use-site control of unfolding - #6354
Conversation
507169f to
721dc26
Compare
|
This looks amazing, will definitely help improve the interface of the standard library! Really looking forward to this landing.
Will there be a new keyword to make an abstract block "not unfold by default" then? |
I think what @amy meant is that standard (non-abstract) definitions are still unfolded by default as in current Agda, in contrast to the language that is presented in the paper where everything is abstract-unless-explicitly-unfolded. One comment I got when explaining this new feature to a colleague is that it has a potential downside for library developers in that they can no longer be sure that changing an |
Thanks, that makes sense!
As does this 👍 |
|
There are two use cases for
I don't think we should allow any kind of unfolding for What about something involving Another option is to drop However, that code is less self-explanatory. |
721dc26 to
0e28d78
Compare
|
Ah, I rebased this by hand, but it didn't even occur to squash the fixups away while I was doing it. Re. the thread, I don't have a strong preference either way on whether this should be added to Edit: actually, I'll say that
has a positive converse: If a library exports an abstract definition together with abstract proofs of its properties, but a necessary property is missing, a user can, while waiting for an upstream fix, unfold the definition and prove it themselves, without having to go in and patch the library by hand. Note that if you use e.g. Nix to manage your Agda libraries, then it's impossible (rather than just inconvenient) to modify libraries by hand |
This is why I mentioned the trick with having a |
|
I think it is a good idea to have an easy way to make a definition really private while keeping its signature public, as @jespercockx suggests. We did not really treat this explicitly in our paper, but I think it is necessary for a serious implementation. I would personally favor having an easy / not-as-encoded way to do this, though it would be fine if it had the same semantics as Jesper mentions. Regarding the naming of |
The Note also that the name For these reasons I am opposed to changing the meaning of |
|
What about |
That's not a bad suggestion in case we decide not to go with |
|
Another option would be to keep the semantics of current |
|
@jespercockx I think that option might be a little confusing, since it conflates two things that are conceptually different (being able to be unfolded, and needing to unfold things). |
|
Well, but in our current implementation these things are conflated: if you want to be able to unfold things, then you yourself also need to be unfolded explicitly. So having the same syntax for both makes some degree of sense. |
|
@jespercockx I think it is not good for those things to be conflated... This should be revisited. Zooming out from implementation details, can you explain why it makes sense for these things to be related? |
|
Note, mostly to self: I'll rebase this on Monday, or when we settle the bike-shedding — whatever comes first 😉 |
If you had a definition of However, this made me consider that even if abstract
f : ...
f = ...
unfolding (f) where
g : ...
g = ...
unfolding (f) where
h : ... g ...
h = ...This would unfold the definition of |
|
Regarding the question of whether it is fine to steal the |
|
Regarding the non-bikeshedding part of this PR, one issue I would still like to see addressed is the fact that currently, |
|
Re I guess the work-around would be to define anonymous modules to contain each of the "abstract blocks" -- would that do it? |
I agree, but I think it's better to not conflate this with the discussion about this new feature (which should preserve backwards compatibility whenever possible).
Yes, that's what I'm doing at the moment in my experiments. |
|
Since the work-around works, I agree that it's better not to conflate. [Need to start planning for Agda 3.0 ...] |
|
@plt-amy Do you want to look into making these two changes (introducing the |
|
What I imagined was to keep it largely orthogonal to abstract/non-abstract, so with four possible states: transparent (the default), abstract, opaque, and abstract+opaque. Abstract+opaque cannot just be the same as abstract, as an abstract definition cannot look through an abstract+opaque definition in the same module. Hence the semigroup instance would be:
Your example would not be accepted, since (This is just a proposal for the design, feel free to shout if this doesn't make sense). |
|
I think that's a sensible approach. It'll take a couple more words to document when something can be unfolded, but it's more consistent than (e.g.)
together with examples of opaque non-abstract not reducing outside of Unfortunately, I won't be able to get my ADHD meds refilled until the 14th, since my psychiatrist's on vacation, so I find it unlikely that I'll be able to focus long enough to implement this until then. If waiting until then is okay, you can leave the implementation to me; but if we don't want to let this PR get in the way of other stuff (or don't want to generate another nasty rebase), @jespercockx I think it'd be better if you could take over (thanks) |
|
I have enough other tasks so if you could do it after the 14th then I can definitely wait until then ;) |
2e8c21d to
fdceed3
Compare
opaque: use-site control of unfolding
…re clauses; add some tests
TODO: write docs for opaque
3d2c025 to
379b2d0
Compare
|
I finally got around to moving the documentation for ¹ I'm writing this comment before CI on the last batch of commits finishes, so there might still be fixups I'll need to squash before merging. |
andreasabel
left a comment
There was a problem hiding this comment.
Wow, great, this has come a long way!
I haven't been following the development...
Agda is a layout-sensitive language, so the new syntax should take advantage of this.
| some-other : Nat | ||
| some-other = 0 | ||
|
|
||
| opaque unfolding (quux ; bar ; foo ; baz ; baz′ ; asdf ; ghij ; some-other) where |
There was a problem hiding this comment.
The parentheses are superfluent, it seems, or is there an example where they are needed to parse?
Also, unfolding should be a layout keyword so that you can drop the semicolons if you like:
opaque unfolding
quux ; bar
foo
baz ; baz′
asdf ; ghij ; some-other
where
...There was a problem hiding this comment.
I can give this a shot since I have worked with layout before.
There was a problem hiding this comment.
Are these parentheses supposed to resemble import using statements?
There was a problem hiding this comment.
That was the idea, yes
There was a problem hiding this comment.
I think the first stab reused the parser for import directives (and thus similar syntax), but in essence, this is a plain identifier list, so no sophisticated parser needed.
unfolding ... where is already a pair of brackets, this is why we do not need to nest another pair inside this.
NB. I am not fond of the using syntax either, but at least there the parentheses have a function (since there is no closing bracket matching using).
There was a problem hiding this comment.
I'm not sure if I like definitions in the same opaque block (i.e. not one in opaque and one in a child unfolding) not to be able to see each-other..
Ah yes I see the problem. The current problem I had with the prototype abstract unfolding from AIM is that all abstract definitions in the same module can see through each other, which is much too coarse. But perhaps having definitions from the same opaque block see through each other would not be so bad, and would make the implementation more straightforward as you said.
In that case, quux should unfold all of foo, bar, and baz since they are in the same opaque block.
There was a problem hiding this comment.
Btw, would be great if we could write unfolding ... in .... However, I don't know if this can be integrated into our layout mechanism, because in has a different role there. Have to have a look.
We discussed a local unfolding construct during the AIM, but the consensus was that we should first implement a minimum viable product with just top-level unfoldings, and then look into extending it later.
There was a problem hiding this comment.
Translating Andreas' example to today's syntax and semantics, we have:
fooandquuxcan unfold each other,barandbazcan unfold each other,- Neither pair of definitions automatically unfolds the other pair.
Nested opaque blocks are treated independently of any enclosing opaque blocks.
Definitions in the same opaque block (in a literal sense) automatically unfold each other, in addition to the transitive closure of the names mentioned in the unfolding clause, if any.
We decided on this because abstract does the same, and it's also very smoothly supported by the current implementation.
My question is, having split "opaque block" from "unfolding block" syntactically, should they be semantically split, too? And if so, how? Should unfolding blocks also unfold everything that belongs to the same enclosing opaque block? What about nested opaque blocks: should they also unfold lexically-enclosing opaque blocks?
There was a problem hiding this comment.
We decided on this because
abstractdoes the same, and it's also very smoothly supported by the current implementation.
Different top-level abstract blocks in the same module are treated as one block. I hope that will not be the case for opaque blocks.
There was a problem hiding this comment.
My question is, having split "opaque block" from "unfolding block" syntactically, should they be semantically split, too? And if so, how? Should unfolding blocks also unfold everything that belongs to the same enclosing opaque block? What about nested opaque blocks: should they also unfold lexically-enclosing opaque blocks?
(Coming back to this issue finally.) I think it makes sense to also make the distinction semantically, even if it is just to provide options for later extensions to the feature. I.e. I think we can currently allow a unfolding block only inside an opaque block.
Regarding the question of nesting, let us try to stick as close as we can to the semantics in the paper. Basically, each opaque block would introduce a new unique identifier (called "proposition symbols" in the paper), and a definition should only unfold if all the identifiers in its surrounding opaque blocks are in the current unfolding set (i.e. their proposition symbols are in the context). A unfolding f where block then should introduce all the identifiers of f to the context, i.e. it should unfold all the definitions in the same opaque block as f as well as the ones in its surrounding opaque blocks, but crucially not those in the 'sister' opaque blocks.
Here is an example:
opaque
f = 1
opaque
g = 2
opaque
h = 3
opaque unfolding g where
k = (f + g) + h
shouldWork : k ≡ 3 + h
shouldWork = refl
shouldFail : k ≡ 6
shouldFail = {! refl !}
Idea
Over AIMXXXI, @jespercockx and I implemented unfolding control for a variation on
abstract, now calledopaque(this is our version of Controlling unfolding in type theory by Gratzer et. al., but in the interest of backwards-compatibility, the default is still "transparent"). Currently, the only way to ‘see through’ anabstractblock is to be in anotherabstractblock, in the same module. Withopaqueblocks, a user can instead write:where the type-checking of
ydepends on knowing the definitional equalityA.x = Nat, i.e., unfoldingA.x. Any definition which unfoldsB.ymust also unfoldA.x, and Agda will compute this transitive closure by itself, so that you can write:If it were possible to write this without unfolding
A.x, then we'd have the definitional equalityB.y : A.x = 123, where123has typeNat, but no equalityA.x = Nat. Note that it's impossible to write the definition of_without first definingC.ty. Quoting the paper:That's precisely the situation we have here, and introducing an auxiliary binding for the type is precisely what they do. The implementation at present does not yet contain
unfolding k1 in Eexpressions (orlet unfolding, or what-have-you). Additionally,abstract unfoldingblocks are "lexically closed", so that the following is valid (within the file we've been building):Implementation
Opacity and abstractness are two separate "directions" of a new type,
IsReducible. The elaborator stores both anAbstractMode, as before, and a newOpaqueMode, which stores theOpaqueIdof the block we're currently in (if any).When a definition gets looked up, the environment "interacts" with the
IsReducibleof the definition, and potentially removes eitherabstract(with the old rules) oropaque(depending on surroundingunfoldingdeclarations). ThePostScopeStatemaintains a mapping fromOpaqueIdtoHashSet QNames, storing the (transitively-closed) sets of names which may be reduced. In addition to theTypeChecking.*changes,opaque unfoldingalso touches the following components:Nicifier: This is the last place where we see
opaqueblocks as coherent pieces of syntax, soOpaqueIds are generated here. We also add an internalNiceUnfoldingdeclaration associating eachOpaqueIdto its user-written list ofQNames.Scope checker: Here we compute the mapping from
OpaqueIds toHashSets ofQNames, initially by traversing the definitions in the block, and after all of those, scope-checking the list in theNiceUnfolding. That last step is where the transitive closure happens: each name in the list must have its ownOpaqueId(otherwise a warning is raised), which allows us to pull in the dependencies of each mentioned name. This also allows us to generate very precise warnings about redundant names inunfoldingclauses.