-
Notifications
You must be signed in to change notification settings - Fork 90
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
Merging recursive record: pondering the semantics #103
Comments
It is not really an answer to your corecursive interpretation, but for the record I wanted to write down here some thoughts on merging and overriding. I think there are two aspects to the problem of how merging and recursive records interact: the semantics and an efficient implementation, and that the second seems the challenging bit. SemanticsIMHO the semantic follows quite straightforwardly from the requirements. We want that recursive fields, which depend on other fields of the same record, refer to the most recent "version":
In this situation, This would allow for a language supported way of doing overriding in Nickel, in contrast with the beginner-disconcerting mechanisms of Nixpkgs and NixOs modules. (Efficient) ImplementationTake 1: dynamic scopingFor late binding to work, we should not bind the occurrence of
But now, if we write this instead:
As soon as Take 2 : functionsThe problem is that we have an implicit dependency in
Field access becomes The semantics is now correct, but we have lost the benefits of laziness, because as expressions are functions, they are re-evaluated each time. Surely we want to only apply this transformation to fields that are actually recursive (assuming that this can be determined syntactically, see the dedicated paragraph in the last section below), and avoid transforming the Take 2 bis: memoizationWell, laziness is about memoization, so let us do that. We just need to not forget the original expression. We attach a thunk to recursive fields together with their functional expression: Towards a build systemThere are still inefficiencies: the thunk of recursive expressions are reset (which potentially duplicates work) at each merge, even if nothing that they depend on is actually modified:
The problem faced here is actually the more general one that a build system tries to solve, at least according to the view of Build systems à la carte. We have a bunch of values (fields) with dependencies and when some of them are modified (here, overridden by a merge), we want to update only those which may be affected. Going down the rabbit hole leads us to bake a build system in Nickel. How deep should we go ?At this point it is not totally clear to me how much all of these performance problems matter in practice. For example, the inefficiency motivating the last section only shows up if we are interleaving field accesses and merges of different generations of records. If the common pattern is rather to first perform a sequence of merges to assemble packages and then exclusively access the fields of the children, the non-sharing of evaluated thunks between parents and children is a non-issue. I feel like the approach of Take 2 : bis is a reasonably simple and efficient first step, on which we can build upon later if necessary. MiscHow does Jsonnet do it ?It is not clear from the specs or the reference how objects and laziness interact in Jsonnet. After taking a quick look at the code, it does not seem that fields using Statically determining dependencyFor our proposals starting from the Take 2 section to be feasible, it is necessary to determine which variables are referring to recursive fields, in order to do the syntactical transformation to a function: Note that this is not always possible in presence of other dynamic binding rules. In particular, in Nix the
It is impossible to know if We can still work this out by mixing the initial approach of take 1 with later steps: instead of transforming expressions to functions of EffectsIn presence of non-idempotent effects, there is also the question of the re-evaluation of effects:
|
I think this discussion is superseded by RFC001 (#330). |
I've been thinking a bit about the problem of overriding à la Nixpkgs.
My thoughts is that it boils down to the semantics of merging recursive records. Or, a bit pedantically, merging codata.
Let me illustrate. Suppose we have a bunch of pkgs:
I want to override
packageA
so thatpackageB
sees the new version. The current way that we think about this is thatpackageA
will have some default values, and to make an override, we will do something likeThis returns a new
nixpkgs
wherepackageA
has been changed (or, rather, made more definite, as per the merging model). The question, then, is: what version ofpackageA
doespackageB
see?So far, we've been thinking of it as if recursive records were data: unfold the record as a tree, and merge recursively. It's a simple model, it makes sense. And
packageB
sees the original version ofpackageA
. But the fact is that, by virtue of the field being lazy, recursive records are, really codata. And maybe, thinking of them this way allows us to define a merging scheme wherepackageB
will see the overridenpackageA
. Pretty much like in #83 (comment) . I haven't thought this through, so I don't know if we can carry this thought to a happy conclusion. But that would certainly be an elegant explanation for overriding.Related issues: #74, #83 .
The text was updated successfully, but these errors were encountered: