|
| 1 | +# Proposal: User-Provided Functions and Validators |
| 2 | + |
| 3 | +Roger Peppe |
| 4 | + |
| 5 | +Date: 2026-03-05 |
| 6 | + |
| 7 | +Discussion at https://github.com/cue-lang/cue/discussions/4293. |
| 8 | + |
| 9 | +## Abstract |
| 10 | + |
| 11 | +We propose adding a Go API for creating CUE-callable functions and |
| 12 | +validators from ordinary Go functions. This provides a simple, type-safe |
| 13 | +mechanism for extending CUE's evaluation with custom logic, without |
| 14 | +requiring the complexity of the existing built-in function infrastructure. |
| 15 | + |
| 16 | +## Background |
| 17 | + |
| 18 | +CUE's built-in functions (in the `pkg/` directory) are implemented using |
| 19 | +an internal framework that is tightly coupled to the evaluator. Adding |
| 20 | +new built-in functions requires modifying internal packages, running code |
| 21 | +generators, and understanding the details of the `adt` package. This |
| 22 | +makes it impractical for users who want to extend CUE with custom logic |
| 23 | +from their Go programs. |
| 24 | + |
| 25 | +There is significant demand for user-defined functions. Common use cases |
| 26 | +include custom validation logic (checking values against external |
| 27 | +databases, applying domain-specific rules), data transformation |
| 28 | +(encoding, hashing, formatting), and integration with external services. |
| 29 | +Currently, the only way to achieve this is to evaluate CUE, extract |
| 30 | +values in Go, process them, and feed results back -- a workflow that is |
| 31 | +both verbose and loses the benefits of CUE's constraint-based evaluation. |
| 32 | + |
| 33 | +The existing built-in function system also has some design choices that |
| 34 | +we would prefer not to carry forward into a user-facing API. In |
| 35 | +particular, some built-ins conflate functions and validators: for |
| 36 | +example, `list.UniqueItems` can be used both as a bare constraint |
| 37 | +(`list.UniqueItems`) and as a function call (`list.UniqueItems()`), with |
| 38 | +identical semantics. This ambiguity complicates the mental model. Some |
| 39 | +built-ins also support optional trailing arguments with default values, |
| 40 | +which introduces a form of variable-arity calling that adds complexity. |
| 41 | + |
| 42 | +We propose a simpler model that avoids both of these issues while |
| 43 | +remaining fully general. |
| 44 | + |
| 45 | +## Proposal |
| 46 | + |
| 47 | +### Functions |
| 48 | + |
| 49 | +We add a family of generic functions to the `cue` package, one for each |
| 50 | +supported argument count: |
| 51 | + |
| 52 | +```go |
| 53 | +func NewPureFunc1[A0, R any](f func(A0) (R, error), opts ...FuncOption) Value |
| 54 | +func NewPureFunc2[A0, A1, R any](f func(A0, A1) (R, error), opts ...FuncOption) Value |
| 55 | +// ... through PureFunc10 |
| 56 | +``` |
| 57 | + |
| 58 | +Each `NewPureFuncN` wraps a Go function of N arguments into a CUE `Value` |
| 59 | +that can be called from CUE expressions. The "Pure" prefix indicates |
| 60 | +that these functions are *referentially transparent*: calling them with |
| 61 | +the same arguments always produces the same result. This property is |
| 62 | +essential for CUE's evaluation model, which may evaluate expressions |
| 63 | +multiple times or in any order. |
| 64 | + |
| 65 | +Arguments are decoded from CUE values to Go types using `Value.Decode`, |
| 66 | +and results are converted back to CUE values. This means that any Go |
| 67 | +type supported by `Decode` (including structs, slices, maps, and types |
| 68 | +implementing `json.Unmarshaler`) can be used as an argument type, and |
| 69 | +any Go type convertible to a CUE value can be used as a return type. |
| 70 | + |
| 71 | +Here is an example of creating and using a simple function: |
| 72 | + |
| 73 | +```go |
| 74 | +ctx := cuecontext.New() |
| 75 | +v := ctx.CompileString(`#add: _, x: #add(3, 4)`) |
| 76 | +v = v.FillPath(cue.ParsePath("#add"), cue.NewPureFunc2(func(a, b int) (int, error) { |
| 77 | + return a + b, nil |
| 78 | +})) |
| 79 | +fmt.Println(v.LookupPath(cue.ParsePath("x"))) // 7 |
| 80 | +``` |
| 81 | + |
| 82 | +Note that the function is injected into the CUE value via `FillPath` |
| 83 | +into a definition (`#add`). This is one way of using them, although |
| 84 | +we anticipate that a more common pattern will be to use value |
| 85 | +injection (see https://github.com/cue-lang/cue/discussions/4294). |
| 86 | + |
| 87 | +If the Go function returns a non-nil error, the CUE expression |
| 88 | +evaluates to bottom with that error message: |
| 89 | + |
| 90 | +```go |
| 91 | +v = v.FillPath(cue.ParsePath("#f"), cue.NewPureFunc1(func(x int) (int, error) { |
| 92 | + if x < 0 { |
| 93 | + return 0, fmt.Errorf("negative value not allowed") |
| 94 | + } |
| 95 | + return x * 2, nil |
| 96 | +})) |
| 97 | +``` |
| 98 | + |
| 99 | +If the wrong number of arguments is passed in CUE, evaluation produces |
| 100 | +an error. |
| 101 | + |
| 102 | +### Validators |
| 103 | + |
| 104 | +We add a generic function for creating validators: |
| 105 | + |
| 106 | +```go |
| 107 | +func NewPureValidatorFunc[T any](f func(T) error, opts ...FuncOption) Value |
| 108 | +``` |
| 109 | + |
| 110 | +A validator is a CUE value that constrains what it is unified with. When |
| 111 | +unified with a concrete value, the validator decodes that value as type |
| 112 | +`T` and calls `f` to validate it. If `f` returns a non-nil error, the |
| 113 | +unification fails with that error. If `f` returns nil, the value passes |
| 114 | +through unchanged. |
| 115 | + |
| 116 | +```go |
| 117 | +ctx := cuecontext.New() |
| 118 | +v := ctx.CompileString(`#v: _, x: #v & "hello"`) |
| 119 | +v = v.FillPath(cue.ParsePath("#v"), cue.ValidatorFunc(func(s string) error { |
| 120 | + if len(s) < 3 { |
| 121 | + return fmt.Errorf("string too short") |
| 122 | + } |
| 123 | + return nil |
| 124 | +})) |
| 125 | +fmt.Println(v.LookupPath(cue.ParsePath("x"))) // "hello" |
| 126 | +``` |
| 127 | + |
| 128 | +Validators are distinct from functions. A function computes a new value |
| 129 | +from its arguments; a validator constrains an existing value. This |
| 130 | +distinction is clear in both the API and the CUE usage: functions are |
| 131 | +called with parentheses (`#f(x)`), while validators are unified with |
| 132 | +`&` (`#v & x`). |
| 133 | + |
| 134 | +This separation does not reduce generality. A function can return a |
| 135 | +validator value, allowing patterns like `#minLen(3) & "hello"` where |
| 136 | +`#minLen` is a function that returns a validator. This composes |
| 137 | +naturally with CUE's existing constraint model. |
| 138 | + |
| 139 | +### Options |
| 140 | + |
| 141 | +Both `NewPureFuncN` and `ValidatorFunc` accept optional `FuncOption` values. |
| 142 | +Currently one option is defined: |
| 143 | + |
| 144 | +```go |
| 145 | +func Name(name string) FuncOption |
| 146 | +``` |
| 147 | + |
| 148 | +`Name` sets the name used in error messages. Without it, error messages |
| 149 | +refer to the function anonymously. |
| 150 | + |
| 151 | +### Exporting |
| 152 | + |
| 153 | +User-provided functions and validators cannot currently be exported to |
| 154 | +CUE syntax. Attempting to format a value containing a function or |
| 155 | +validator produces an error message indicating this limitation. This is |
| 156 | +an area for future work. |
| 157 | + |
| 158 | +### Restrictions |
| 159 | + |
| 160 | +This initial proposal deliberately omits several capabilities: |
| 161 | + |
| 162 | +- **Variable-arity functions.** Each `NewPureFuncN` requires exactly N |
| 163 | + arguments. There is no support for optional arguments or variadic |
| 164 | + calls. |
| 165 | + |
| 166 | +- **Non-concrete arguments.** All arguments must be fully |
| 167 | + resolved CUE values. Non-concrete argument values are not currently |
| 168 | + supported, although they could be. |
| 169 | + |
| 170 | +- **Side effects.** Only pure functions are supported. Functions that |
| 171 | + perform I/O or maintain state are not appropriate for use with this |
| 172 | + API, as CUE's evaluator makes no guarantees about evaluation order or |
| 173 | + frequency. |
| 174 | + |
| 175 | +These restrictions keep the initial implementation simple and the |
| 176 | +semantics clear. Each could be relaxed in future proposals if there is |
| 177 | +demand. |
| 178 | + |
| 179 | +## Rationale |
| 180 | + |
| 181 | +### Why separate functions and validators? |
| 182 | + |
| 183 | +The existing built-in system's conflation of functions and validators |
| 184 | +(`list.UniqueItems` vs `list.UniqueItems()`) is a source of confusion. |
| 185 | +Separating them in the user-facing API makes the mental model clearer: |
| 186 | +functions compute, validators constrain. Since a function can return a |
| 187 | +validator, there is no loss of expressiveness. |
| 188 | + |
| 189 | +### Why fixed arity? |
| 190 | + |
| 191 | +Supporting variable-arity functions would require either a |
| 192 | +`...interface{}` argument (losing type safety) or a more complex |
| 193 | +registration mechanism. Fixed arity with generic type parameters gives |
| 194 | +us compile-time type safety in Go while keeping the API surface small. |
| 195 | +The maximum of 10 arguments covers the vast majority of practical use |
| 196 | +cases. |
| 197 | +
|
| 198 | +### Why NewPureFunc and not just NewFunc? |
| 199 | +
|
| 200 | +The "Pure" prefix is intentional. CUE's evaluation model assumes that |
| 201 | +expressions are deterministic. Making purity explicit in the name serves |
| 202 | +as documentation and a reminder. It also leaves room for a hypothetical |
| 203 | +future `Func` (or similar) that might support side effects in a |
| 204 | +controlled manner, perhaps in the context of CUE scripting. |
| 205 | +
|
| 206 | +### Why not use the existing built-in framework? |
| 207 | +
|
| 208 | +The existing framework is designed for functions that ship with CUE |
| 209 | +itself. It uses code generation, internal types, and conventions that |
| 210 | +are not suitable for an external API. A separate, simpler API is more |
| 211 | +appropriate for user-provided functions. |
| 212 | +
|
| 213 | +## Compatibility |
| 214 | +
|
| 215 | +This proposal adds new public API surface to the `cue` package. It does |
| 216 | +not change existing CUE evaluation semantics or syntax. No existing code |
| 217 | +is affected. |
| 218 | +
|
| 219 | +The `NewPureFuncN` naming pattern reserves a range of names in the `cue` |
| 220 | +package. We do not expect this to conflict with any likely future |
| 221 | +additions. |
| 222 | +
|
| 223 | +## Implementation |
| 224 | +
|
| 225 | +The implementation adds the following to `cuelang.org/go`: |
| 226 | +
|
| 227 | +- `cue/func.go`: The `ValidatorFunc` function and the internal |
| 228 | + `pureFunc` generic implementation. |
| 229 | +- `cue/func_gen.go`: Generated `NewPureFunc1` through `NewPureFunc10` |
| 230 | + functions, produced by `cue/generate_func.go`. |
| 231 | +- `internal/core/adt`: Two new types, `Func` (implementing the `Expr` |
| 232 | + interface for callable functions) and `FuncValidator` (implementing |
| 233 | + the `Validator` interface). These integrate with the evaluator's |
| 234 | + existing function-call and validation machinery. |
| 235 | +
|
| 236 | +A proof-of-concept implementation exists. See |
| 237 | +https://cue.gerrithub.io/c/cue-lang/cue/+/1232921. See also the |
| 238 | +companion proposals on [value injection](./4294-value-injection.md) |
| 239 | +(which provides a mechanism for injecting functions into CUE packages |
| 240 | +without `FillPath`) and [tagged string |
| 241 | +literals](./language/4295-tagged-string-literals.md) (which |
| 242 | +demonstrates a use case for user-provided functions). |
| 243 | +
|
| 244 | +## Future work |
| 245 | +
|
| 246 | +Several directions could build on this foundation: |
| 247 | +
|
| 248 | +- **Function signature declarations.** A CUE syntax for declaring the |
| 249 | + expected signature of a function value, enabling static checking of |
| 250 | + call sites. |
| 251 | +- **Keyword arguments.** Support for named arguments, which would |
| 252 | + compose naturally with CUE's struct-based data model. |
| 253 | +- **Custom CUE binaries.** A mechanism for building a `cue` binary that |
| 254 | + includes custom function packages, similar to how Caddy allows custom |
| 255 | + modules. |
| 256 | +- **RPC protocol.** An IPC-based protocol for executing custom functions |
| 257 | + in a separate process, removing the requirement that functions be |
| 258 | + implemented in Go and enabling language-agnostic extensibility. |
0 commit comments