Replies: 2 comments
-
In a follow-up conversation with @rogpeppe we observed the following regarding the use of #workflowMatchN: {
#step: matchN(1, [{
run!: string
}, {
uses!: string
}])
jobs!: [string]: steps!: [...#step]
}
#extendedWorkflowMatchN: {
#workflowMatchN
jobs!: [string]: steps!: [...{
#run?: string
run?: =~"^set -o nounset\n"
if #run != _|_ {
run: """
set -o nounset
\(#run)
"""
}
// do not apply this to the uses leg of the disjunction
uses?: _|_
}]
}
w2: #extendedWorkflowMatchN & {
jobs: test: steps: [{
#run: "hello world"
}]
} Note how today the widening of the elements in the
Indeed, if we were to implement #543, because of point 1 I think it would be impossible widen elements in this way "through" the This highlights at least a couple of points:
These points (and likely others) contribute to the same question/observation as before: is creating an abstraction in this way the right thing to do? Compared, say, to treating the problem as a data transformation exercise. Not looking to draw any particularly conclusions from this further observation, just adding more notes. |
Beta Was this translation helpful? Give feedback.
-
https://cuelang.org/cl/1206677 contained an implementation of the approach described in the original description for this issue. However, that stack under https://cuelang.org/cl/1206677 has been abandoned in favour of https://cuelang.org/cl/1206945, because the latter is a better solution to the more immediate problem presented by #3603. The general "problem" that motivated this discussion however remains, and I will post details of a further example to potentially broaden the discussion, but also to ensure we don't get "stuck" on #3603 where (in this case) there is a better solution. |
Beta Was this translation helpful? Give feedback.
-
#1576 contains discussion about the fact that embedding a definition recursively opens the definition at the site of the embedding to allow for additional fields and definition (play):
This behaviour is per the spec:
However, #1576 makes an interesting observation that definitions referenced by the embedded definition are also opened (play):
The perhaps surprising thing here is that
#Foo2
allows the addition of the fieldbar?: string
to the list element of type#Bar
: it was#Foo
that was embedded,#Bar
was simply originally referenced by#Foo
. Per @mpvl, this is per the spec:Indeed if definitions were not opened recursively as this behaviour demonstrates, then it would be impossible (without additional machinery) to allow
#Foo2
to widen (with respect to closedness) the type of element allowed in thebars
field.That said, #1576 appears (from my initial understanding, and indeed re-reading) to be more oriented towards a problem that would be solved by a proposed
must()
builtin (also mentioned in #943).However the "recursive opening of a definition referenced by the embedded definition" aspect caused me to revisit this #1576 in the context of the following problem, and in doing so touch on some of the benefits/issues with respect to definitions, closedness, embedding and the like.
My "problem"
All the CUE repos use GitHub Actions for CI, and correspondingly we use the GitHub Actions workflow schema to validate our CI declarations. At a very much simplified level, a workflow schema (and example workflow instance) looks like this (play):
In the course of a "Friday hack" exploration of a solution for #3603, I considered doing the following (play) to template in the setting of a bash option:
This works. The embedding of
#workflow
, per the spec, recursively opens all definitions, allowing additional fields to be declared. Indeed because we have not yet enforced #543 we could add a#run
field without needing to embed#workflow
, but given #543 feels like the right thing to do, it's appropriate to present this example as if it were implemented.The addition of the
#run
field is in effect widening the type of#cueworkflow
with respect to#workflow
: the set of possible values for a value of#cueworkflow
is greater than those possible with#workflow
.(However this widening specifically affects the set of fields allowed, not the values of existing fields. i.e. it is impossible via embedding to widen a field
f?: string
tof?: _
.)Ok, so what's the problem?
Given that I appear to have a solution here, we are "job done" through one lens. We have been able to reuse the
#workflow
definition, and in doing so present a neat abstraction on top of the existing structure. The data transformation from the#run
field to the targetrun
field is neatly described in terms of regular CUE (regular CUE that would be greatly improved readability-wise with the additions proposed in #943) in a way that is clear to the user. The user of#cueworkflow
does not need to think about an entirely different structure to that with which they are familiar: they simply have to use#run
instead ofrun
(because the latter is set for them). This approach is also relatively forward compatible: if the authors of#workflow
make a change that causes our abstraction augmentation to break in some way, we will know about it. That, as the authors of#cueworkflow
, is a risk we accept in reusing the structure of#workflow
in this way.But is this the best/right approach?
Definitions are recursively closed on reference. Should we instead shift to a more explicit approach through some additional syntax/builtin? e.g.
rclose(workflow)
? This would require users of such schemas to have to add additional syntax at each "call" site in order to validate that structure does not declare additional fields, catch typos. Such an approach would, however, make much more explicit and precise the locations at which "typo checking" is expected.We are nearly 100% reusing the structure of
#workflow
here, which gives a familiar feel to the end user. If we were to choose a different interface, what would it look like? At what point does the benefit of providing a "clean" interface surpass the cost of having to learn a new abstraction?Should we instead be treating this as a data transformation problem? i.e. we continue to use
#workflow
as is, but somehow specify that the exported concrete configuration value is a transformed version of the input, where eachrun
field value is prefixed by the lineset -o nounset
? That would likely require some version of #165, https://github.com/myitcv/cuetransform or equivalent, to allow us to express the data transformation in a clean, path-oriented way. Such an approach could however "bury" the transformation from the user in a way that might cause confusion: "where did thisset -o nounset
line come from?".Conclusion?
No real hard conclusions, just some observations:
#{ x?: string }
, rather than relying on definitions.#workflow
structure) to do so trivially. Perhaps a more explicit mechanism of recursively "removing" closedness would be more appropriate, but this feels like a relatively minor detail.steps
list by adding the#run
field, but cannot changerun?: string
torun?: any
for example.#cueworkflow
unless careful. Placing#cueworkflow
in a separate package from its usage sites and adding some tests would help to mitigate any risks of "other" fields accidentally being unified.#cueworkflow
, with our additions being "broken" by changes from authors upstream to#workflow
.References
Would very much welcome thoughts/etc from others in this space. I started this discussion really to briefly flesh out my thoughts in this space. But I fully acknowledge this space is much bigger than the scope of what I cover here. I make no claim to have covered that entire space; indeed I can't hold the entire space in my head! So this first post is an attempt to iteratively tackle some aspects.
cc @mpvl @verdverm @rogpeppe @cuematthew based on (recent) conversations/interactions
Beta Was this translation helpful? Give feedback.
All reactions