Repository navigation
Replies: 4 comments 11 replies
|
The assertion that optional fields don't exist is hard to accept. This area of CUE is still hard to understand. My coworkers have asked me several times recently what it means for an optional field to have a concrete value, or to have a default value. My initial take is that You shared this example: a?: 123
x: isConcrete(a)I read that and think, " a?: 123
a: _ // Effectively no statement, other than that 'a' should be exported.
x: isConcrete(a)Now, you could say, "Well, that's a different question; you're asking about a different Again, I read the |
|
Does this provide a way to check for an optional field having a value, but raising an error if that field can never exist? There is the combination of |
|
Some feedback from a full-scale adoption exercise. helm2cue generates CUE from Helm charts, and its output leans heavily on the historical Overall: it works, and the output reads far better than the bottom comparisons. But having looked harder at why it works, my headline finding has changed: the drop-in only works because
1.
|
|
I have updated the proposal above based on the feedback in this thread and a more detailed look at the edge cases. A summary of what changed, per thread: @mxey: we ended up making
This also means we no longer need the @seh: @myitcv: both your findings led to changes, though one went in a different direction than you suggested. I believe it still covers the helm2cue cases:
Some other changes:
The proposal body above has been updated to reflect all of this. As before, some of this may still change based on feedback. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Proposal: predicate builtins
Summary
Add a family of predeclared builtins that let CUE inspect a value's state — whether a field is present, whether an expression is valid, whether it is concrete, and whether a label is permitted — returning a boolean without propagating errors. They are first-class, well-defined replacements for the long-standing comparison-to-bottom idiom (
!= _|_/== _|_).The set:
exists(ref)isValid(expr)exprstill be made concrete, without error?isConcrete(expr)exprevaluate to a concrete value?existsN(n, refs…)nof the referenced fields exist?countN(n, [conds…])nof the boolean conditions true?reflect.Allows(s, label)label(string field / int index) ever appear ons? (stdlib)reflect.Field(s, name)namehas, or would have, ins(stdlib)In the
…Nvalidators,nis the same kind of count constraint: a literal requires exactly that many, and a bound such as>=2or<=1requires at least or at most that many.The predeclared builtins are gated behind a new per-file experiment,
@experiment(predicates)(preview in v0.18.0); thereflectfunctions are ordinary standard library functions. The normative semantics and worked examples are in the language spec (doc/ref/spec.md).This supersedes the relevant part of #943, with the other builtins it mentioned spun out separately (e.g.
mustis already tracked in #575).Motivation
Users routinely need to branch on properties that CUE's monotonic, error-propagating evaluation otherwise hides — is this field present? did this evaluate without error? is it concrete yet? Today this is written as a comparison to bottom:
That idiom is problematic:
Giving each question its own named builtin makes the intent explicit and the semantics specified, and lets the evaluator surface order-dependence as a cycle rather than a silent wrong answer.
A concrete driver is protobuf's
oneof— "exactly one of these fields is set." That is currently awkward to encode in CUE (it needs a disjunction of structs or hand-written constraints);existsN(1, a, b, c)expresses it directly.countNgeneralizes this "N of M" pattern from presence to arbitrary boolean conditions.Proposal
All builtins are predeclared and gated behind
@experiment(predicates); thereflectfunctions are ordinary standard library functions.Design principles
Three rules determine most of the edge-case behavior:
isValidconverts a permanent error into a boolean; every other builtin propagates errors like ordinary evaluation, so the predicates never silently mask a broken configuration. (The*f(x) | fallbackdisjunction remains the way to tolerate errors at the expression level.)falsealways means "not now, but a legal configuration could add it"; a question whose answer can never be true — a mistyped field on a closed struct — is an error. Whether a field is possible is judged from the declarations, not from whether its constraint is satisfiable.Functions (
→ bool)exists(ref)— whether the referenced field is present as a regular member. A field that is absent but could still be added isfalse: a declared optional or required field without a value, an undeclared field on an open struct, a label matching a pattern constraint, or an index just beyond an open list. A field that can never exist is a permanent error: an undeclared field on a closed struct or definition, an out-of-range index of a closed list, or a field of a scalar. Presence is independent of the field's value: a present field whose value contains an error still exists.refmust be a reference (field reference, selector, or index expression); a non-reference argument is an error.To probe a field that may be impossible, compose:
*exists(cfg.tls) | falsenever errors, andreflect.Allows(cfg, "tls") && exists(cfg.tls)guards the probe (&&short-circuits). The fallback form also covers chained paths through absent intermediate fields, as in.Values.a.b.c-style templating:*exists(vals.absent["k"]) | falseisfalse, not an error.isValid(expr)—trueiffexprcan still be made concrete, without error, by unifying it with some value. It isfalsewhen the expression's own result is an error of any kind — a permanent error, or an unresolved or absent reference — and when a permanent error is reachable in some instance of it. Values that are merely unfinished are valid: abstract types, unresolved disjunctions, and incomplete interior fields. Constraints that no instance is required to satisfy do not invalidate a value.Note that it matters where an incomplete part sits: with
b: {x: y + 1, y: string},isValid(b)istrue(the struct is a fine partial value) whileisValid(b.x)isfalse(the expression's own result is incomplete). Also,isValidreports; it does not rescue: a broken regular field still makes the configuration fail, whateverisValidreturns.isConcrete(expr)— whetherexprevaluates to a fully concrete value, as export requires: recursively for structs and lists, with no required field left unset. Optional fields, pattern constraints, and open list tails play no role. An absent field is not concrete, so the answer isfalserather than an error. A permanent error is passed through: whether a broken value is concrete is not a meaningful question. To tolerate broken values deliberately, writeisValid(x) && isConcrete(x).Count validators
existsNis embedded in a struct to constrain how many of the referenced fields exist, and directly encodes protobuf'soneofand its relatives. It is shorthand forcountNoverexistsconditions —existsN(n, a, b, c)is equivalent tocountN(n, [exists(a), exists(b), exists(c)])— and each reference has the semantics ofexists, including strictness: a mistyped reference is an error, not a silently uncounted condition.countN(n, [conds…])is the general form: it constrains how many conditions in a list of booleans are true, so it combines with any predicate, and the list may be computed:The two argument shapes are deliberate:
existsNtakes its references as separate arguments because references are syntax, not values — in a list literal they would be evaluated — whilecountNtakes a list because CUE has no spread operator, so the list form is the only one that admits computed conditions.validNandconcreteNfrom the earlier version of this proposal are subsumed and removed.A permanent error in a condition propagates; a condition that is still incomplete when the configuration is finalized is an error rather than being counted as false. If a count constraint can never be satisfied, validation fails immediately; otherwise it stays incomplete until a definitive answer can be determined. Once determined, a violation is a permanent error, so
isValid(countN(...))converts a validator outcome into a boolean.Standard library
The
reflectpackage houses the value-inspection functions. They are ordinary functions and need no experiment.reflect.Allows(s, label)— whetherlabel(a string field name or integer index) may ever appear on the struct/lists. It is the structural "may ever appear" counterpart toexists, and never errors whereexistsdoes: for an undeclaredkon a closeds, boths.kandexists(s.k)are errors, whilereflect.Allows(s, "k")isfalse.reflect.Field(s, name)— the value that the fieldnamehas, or would have, ins, defined as the value ofnameins & {(name): _}: a present field's value; a declared optional or required field's constraint, with applicable pattern constraints unified in;_for an undeclared name on an open struct; an error for a name that can never exist. This makes the constraint of an absent field observable:reflect.Fieldalone cannot distinguish an absent field from a present one whose value is_; combine it withexistsorallowsto disambiguate.Resolved questions
The two open questions of the original proposal are resolved as follows.
What does
isValidclose over?The configuration:
isValidreads its argument once the configuration is finalized, so anything the configuration itself supplies counts, and nothing hypothetical does. In the examplexisfalse— when the configuration is finalized,ahas no value — and becomestruethe moment a sibling suppliesc: 5. A stricter reading, "satisfiable by unifying the value in isolation," remains expressible later via a self-containedness predicate, which is left for future work.What does
isConcretereport for an optional or required field?false, rather than an error: a field that is not present is not concrete, so the question has a definite answer. This also makes the guardif isConcrete(a) {...}usable for its main purpose.isConcrete({a?: 123})remainstrue: the optional field plays no role, and the struct exports fine. To ask about the value an optional field would have, rather than the field itself, usereflect.Field(see above).Relationship to #943
#943 is stale (it predates
allowsandisValid), too broad (it also coversmust,range, …), and has accumulated unrelated discussion. This proposal pins down the concrete, implemented design for the presence/validity/concreteness predicates, and deliberately uses the namepredicatesrather than "reflect"/"introspection" — reserving the latter for possible future capabilities (subsumes, self-containedness), which are explicitly left open. Constraint access, previously in that category, landed asreflect.Field.All reactions