-
-
Notifications
You must be signed in to change notification settings - Fork 175
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
Add support for with
keyword
#923
Conversation
Fixes #114 Fixes #340 Related to #754 This adds a new keyword which is syntactic sugar for using `//` to perform a deep override of the record. This comes in handy when working with very large records (such as those found in `dhall-kubernetes`). This similar in spirit to Kustomize, albeit not as powerful and customizable as Kustomize. The goal is that the user can write the following code to update an existing default configuration: ```dhall someDeployment with { , metadata.labels = someDeployment.metadata.labels # toMap { `scheduler.alpha.kubernetes.io/critical-pod` = "" } } ``` You can also see this idiom outside of the Dhall/Kuberentes ecosystem. For example, many Nix users manually apply deeply nested updates by hand, like this example from the Nixpkgs manual: ```nix { haskell = super.haskell // { packages = super.haskell.packages // { ghc784 = super.haskell.packages.ghc784.override { overrides = self: super: { ghc-events = self.callPackage ./ghc-events-0.4.3.0.nix {}; }; }; }; }; }; ``` ... where the Dhall equivalent would be: ```dhall super with { haskell.packages.ghc784 = super.haskell.packages.ghc784.override { … } } ``` There is one additional change that I plan to make that builds on top of this which is to would like to have the record completion syntax desugar to use `with` instead of `//`, like this: ```dhall T::r = (T.default with r) : T.Type ``` ... which would allow for deep overrides for the record completion operator, too. However, I chose to defer that to a separate change to keep this change as minimal as possible.
... as standardized in dhall-lang/dhall-lang#923
tests/normalization/success/unit/WithMultipleNestedUpdatesA.dhall
Outdated
Show resolved
Hide resolved
IIUC the implementation, this won't work for Optional nested record, as described in #924 . Could this be supported in the future? I guess this would requires a bit of syntax to differentiate optional, for example using: |
... as suggested by @Nadrieril
... as suggested by @Nadrieril
... to avoid a desugaring algorithm with exponential code complexity ... as suggested by @Nadrieril
... for consistency with other files in the same directory
... which verifies that they behave the same as multiple updates for a single `with` ... as suggested by @Nadrieril
... to make room for a simpler test for the base case
... based on a discussion with @Nadrieril
I forgot to mention that the matching change to the Haskell implementation is here: dhall-lang/dhall-haskell#1685 |
I find it rather tricky to wrap my head around the desugaring rules. Ultimately I'm somewhat uncomfortable that the "override-record" looks like a record but isn't one. I expect this to cause quite a bit of confusion. Say, for example, you want to change { a.b = 1 } with { a.c.d = 2 } but that fails with
What you have to write is { a.b = 1 } with { a.c = { d = 2 } } I think it's not very easy to understand why only the second version works, especially since the "override-records" are equivalent when interpreted as actual records. So I want to bring up two alternatives which might help: 1. Pick a syntax for the updates that doesn't look like record literals. For example, if the updates look more like list literals, that might emphasize the importance of the ordering: Instead of record with { a = x, b = y } we could write record with [ a = x, b = y ] 2. How about implementing this via β-normalization instead of desugaring?! Could that help us regain confluence even when we treat the overrides as a proper record? If we add proper type-checking, that should also help with the error messages. My impression is that this feature might be too complex to handle it via desugaring. |
@sjakobi @Nadrieril: What if I were to retract this proposal and replace it with another proposal that does the following:
Most of the complexity would be in explicitly enumerating which expressions fit into the latter two categories (i.e. immediate non-record literals vs. abstract expressions), but the upside is that the update expression could then be a real record literal and desugaring/resugaring the update record to add/remove dotted labels wouldn't change the behavior. |
That sounds appealing, but then that means there is no way to deeply replace a whole record, and that might be something we want. let update
: ∀(T : Type) → ∀(U : Type) → U → { contents : T } → { contents : U }
= λ(T : Type)
→ λ(U : Type)
→ λ(thing : { contents : T })
→ λ(newContents : U)
→ thing with { contents = newContents }
in ... With the new proposal, this would be equivalent to: let update
: ∀(T : Type) → ∀(U : Type) → U → { contents : T } → { contents : U }
= λ(T : Type)
→ λ(U : Type)
→ λ(thing : { contents : T })
→ λ(newContents : U)
→ { contents = thing.contents with newContents }
in ... What happens if we instantiate update { _1 : Bool } { _2 : Natural } { _1 = True } { _2 = 0 }
=== { contents = { _1 = True } with { _2 = 0 } }
=== { contents = { _1 = True, _2 = 0 } } We just broke type soundness: the return value of More generally, we would need an equivalent of How about we change the syntax however, and change it to record
with x.y.z = 42
with a.b.c = False
with foo.bar?.baz = Some "string" Then the confusion with records disappears. |
@Nadrieril I like the syntax you propose! My example from #923 (comment) would look like this, right? { a.b = 1 } with a.c = { d = 2 }
=> { a = { b = 1, c.d = 2 } } To replace the entire { a.b = 1 } with a = { c.d = 2 }
=> { a.c.d = 2 } And this would still be illegal I assume (although it might be nice if we could make it work): { a.b = 1 } with a.c.d = 2 Regarding the |
@Nadrieril: Yeah, I like your idea. I'll amend this proposal to use that syntax instead |
... as suggested by @Nadrieril This requires changing the relative precedence of `with-expression` and `application-expression`
@Nadrieril @sjakobi: Alright, I updated the proposal so that the new syntax is |
... as caught by @sjakobi
... by adding the case-sensitive `with` rule and using that instead of the case-insensitive "with"
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.
Could you add another parser test that updates a field named Some
?
I'm very happy how this proposal has shaped up, and I'm honestly a bit proud to have been involved in the discussion. :)
Great work!
I've thought more about how I would implement this and I have changed my thoughts a bit:
Considering this, I'd prefer to have |
FWIW I played a bit with the Haskell implementation and thought that the error messages were mostly okay, e.g.
This one wasn't so good though, even with
Maybe there's a simple way to improve this error? |
This is the ideal I had in mind (in the style of
The |
@Nadrieril: What if we used the originally simpler desugaring without the As far as error messages go, the Haskell implementation already has a separate constructor for |
Huh, that option hadn't crossed my mind, thanks. I was too attached to having my AST match the one specified by the binary encoding. With this new option open, I'm way less concerned about the particular choice we make for this PR.
Yeah, I'm happy with that. Since I now understand that implementations are free to do something more clever I don't believe this is a major problem anymore. |
... after discussion with @Nadrieril
... as suggested by @sjakobi
* Implement `with` keyword ... as standardized in dhall-lang/dhall-lang#923 * Implement new desugaring logic * Update `dhall-lang` reference * Update to reflect new `with` syntax * Update to latest draft of proposal * Switch to simpler desugaring * Relocate `desugarWith` back to `Dhall.Syntax` module * Fix downstream build failures * Update `dhall-lang` to `master` * Improve formatting of `with` expressions ... as suggested by @sjakobi * Fix warnings * Improve error messages ... both for `with` and `//` ... as requested by @sjakobi * Use a `PreferAnnotation` type ... to record the origin of the `Prefer` constructor ... as suggested by @sjakobi * Preserve the original `with` expression ... in order to improve the error for `with` * Fix error message for `MustCombineRecord` ... as caught by @sjakobi * Fix build failure for benchmark suite * Fix test build failure * Fix property test failure * Fix benchmark build failure ... for real this time 😅 * Add missing documentation for `PreferAnnotation` * Add tutorial documentation for `with` ... and for nested labels, which was missing * Fix `dhall-nix` build failure Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Simon Jakobi <simon.jakobi@gmail.com>
@is the |
@sjakobi Thanks. This "with" keyword is a difficult candidate for the search box ;-) |
The motivation for this change is: dhall-lang/dhall-haskell#1960 … where a deeply nested and chained `with` expression caused a blow-up in the size of the desugared syntax tree. This alternative desugaring was actually an idea we contemplated in the original change that standardized `with`: #923 … but at the time we elected to go with the simpler desugaring, reasoning that users could work around performance issues with explicit `let` binding: #923 (comment) However, for chained `with` expressions that work-around is not ergonomic, as illustrated in dhall-lang/dhall-haskell#1960. Instead, this adds language support for the `let` work-around. The change to the `WithMultiple` test demonstrates that this leads to a more compact desugared expression. Before this change the expression `{ a.b = 1, c.d = 2 } with a.b = 3 with c.e = 4` would desugar to: ```dhall { a.b = 1, c.d = 2 } ⫽ { a = { a.b = 1, c.d = 2 }.a ⫽ { b = 3 } } ⫽ { c = ({ a.b = 1, c.d = 2 } ⫽ { a = { a.b = 1, c.d = 2 }.a ⫽ { b = 3 } }).c ⫽ { e = 4 } } ``` … and now the same expression desugars to: ```dhall let _ = let _ = { a.b = 1, c.d = 2 } in _ ⫽ { a = _.a ⫽ { b = 3 } } in _ ⫽ { c = _.c ⫽ { e = 4 } } ```
Fixes #114
Fixes #340
Related to #754
This adds a new keyword which is syntactic sugar for using
//
toperform a deep override of the record. This comes in handy when
working with very large records (such as those found in
dhall-kubernetes
). This similar in spirit to Kustomize, albeit notas powerful and customizable as Kustomize.
The goal is that the user can write the following code to update an
existing default configuration:
You can also see this idiom outside of the Dhall/Kuberentes ecosystem.
For example, many Nix users manually apply deeply nested updates by
hand, like this example from the Nixpkgs manual:
... where the Dhall equivalent would be:
There is one additional change that I plan to make that builds on top of
this which is to would like to have the record completion syntax desugar
to use
with
instead of//
, like this:... which would allow for deep overrides for the record completion
operator, too. However, I chose to defer that to a separate change to
keep this change as minimal as possible.