Replies: 3 comments 3 replies
|
Can you define constraints between parameters? Say, a |
2 replies
|
I'm delighted to see a proposal that addresses one of CUE's oldest feature requests!
I'd like to hear how you would describe the similarities and differences to ideas floated in previous discussions, and what changed that made this able to land now. |
1 reply
|
I see no explicit provisions for private/intermediate values. Is the intended approach for that to use a struct? f: func(a: int) -> int : {
op1: a * 2
out: op1 + 1
}.out |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 Details:
Proposal: functions in CUE
This proposal adds first-class functions to CUE. Today, users often imitate functions with structs, for example
(sum & {a: 1, b: 2}).out. That pattern is verbose. It has no positional calling and no arity checking, and it doesn't separate parameters and results from ordinary fields. When something goes wrong, error messages point at struct internals instead of at a call. CUE also already has functions in the standard library, but their signatures can't be written in CUE itself. So builtins can't be documented precisely, higher-order parameters can't be typed, and there is no way to check that a user-supplied function matches an expected interface.The solution is a function literal:
func, parameters in parentheses, an optional result constraint after->, and a body after:. You can call a function by position, by label, or both, with positional arguments first:Parameters come in five forms, ranging from position-only to name-only:
int): supplied by position only._~x: int): supplied by position; the body calls itx, which can be renamed freely.a: int): callable both ways.a!: int): supplied by label only.a?: int): may be omitted; supplied by label only.A default is declared explicitly with
=. It applies only when the argument is omitted, so an argument that carries its own disjunction default reaches the body unchanged. This fixes a real flaw in an earlier design that reused CUE's*defaults: the caller's default and the parameter's default would cancel each other out.A literal without a body is a function type. Unifying a function type with a function value tightens the value, and the type's constraints are checked on every call. A trailing
...makes a type open, so it accepts extra parameters. Because constraints are checked per call rather than when types are unified, you can bind an idealized signature likefunc(int) -> intto a stricter implementation, such as one limited to 64 bits. Function types can appear anywhere a value can, so higher-order functions need nothing extra. A call ending in..., such asadd(1, ...), is a partial application that returns a function over the remaining parameters. Recursion, direct or mutual, is reported as a structural cycle, so CUE stays non-Turing-complete. Nested and repeated calls are still fine. Function values are opaque: a function unifies with itself and with function types, and with nothing else.Conceptually, each call builds a fresh struct of parameter values, unifies the arguments with their constraints, and returns the body unified with the result constraint. This lets users reason about functions with the lattice they already know. The experimental evaluator runs functions natively and memoizes results per call site, which keeps deeply nested calls linear in cost. The proposal also defines backwards compatibility for evolving signatures. A change is compatible if every call the old signature accepted still binds and type-checks (inputs may only widen) and every new result is still an instance of the old result type (results may only narrow). Names and positions are one-way commitments: you can add a way to call a parameter, for example making a name-only parameter positional, but taking one away breaks callers. That is the reverse of Python's situation. A table in the proposal classifies common changes, such as adding a defaulted parameter (compatible) or renaming a named parameter (breaking).
The feature is enabled per file with
@experiment(functions). A file without it parses exactly as before:funcstays an ordinary identifier there, and->and labeled arguments are gated. Modules that use functions can therefore depend on older modules and be depended on by them. When the experiment graduates to a language version,cue fixcan drop the attribute and, iffuncbecomes fully reserved, quote existing fields namedfunc. Several things are left out on purpose. Foreign function interfaces, purity, and side effects are a main motivation but go to a follow-up proposal. Variadic arguments are passed as lists, as builtins already do. References between parameters are reserved as an error so that dependent parameter types remain possible later. Overloading through disjunctions of functions is also deferred.[Summary generated by Claude AI]
Full Proposal
The complete proposal with all technical details, examples, and implementation notes can be found in the proposal document.
How to Comment
Please provide feedback on this GitHub discussion.
All reactions