Foundations



JSON Superset

Cue is a superset of JSON, it includes everything JSON has and then adds some extras. This means Cue can read all JSON and represent it, while not all Cue is representable as JSON. Rest assured this is a good thing because it makes using Cue familiar and also more expressive. When representing JSON in Cue, the differences are:

  • There are // single line comments
  • Objects are called structs
  • Object members are called struct fields
  • Quotes can be omitted for field names without special characters
  • You don’t need commas after struct fields
  • You can place a comma after the last element in a list
  • The outermost curly braces are optional

superset.cue

str: "hello world"
num: 42
flt: 3.14

// Special field name (and a comment)
"k8s.io/annotation": "secure-me"

// lists can have different element types
list: [
	"a", "b", "c",
	1,
	2,
	3,
]

obj: {
	foo: "bar"
	// reuse another field?!
	L: list
}

cue export superset.cue --out json

{
    "str": "hello world",
    "num": 42,
    "flt": 3.14,
    "k8s.io/annotation": "secure-me",
    "list": [
        "a",
        "b",
        "c",
        1,
        2,
        3
    ],
    "obj": {
        "foo": "bar",
        "L": [
            "a",
            "b",
            "c",
            1,
            2,
            3
        ]
    }
}

The Lattice

Every instance lives somewhere in Cue’s Value Lattice (wikipedia). The most open value is “top” or _ which matches any instance. The most restricted value is “bottom” or _|_ and represents invalid instances, conflicts, and errors. Every other instance is in between, and partially ordered when compared to other instances. This may sound confusing right now, but will become clearer as you learn and use Cue more. It’s based on some mathematical concepts you can learn more about in the theory section. For now, consider top -> schema -> constraint -> data -> bottom as a rough guide with instances becoming more concrete until there is an error.

Types are Values

If you think about JSONSchema vs JSON, they are separate concepts. One defines a schema, the other is data. In Cue they are the same.

Cue merges types and values into a single concept, the value lattice. This gives us the ability to define schemas, refine with constraints, fill with data, and combine these ideas along a spectrum. It also means defining schemas is more natural with how we think about and write code as humans.

$ cue eval schema.cue constraints.cue data.cue

schema.cue

album: {
	title: string
	year:  int
	live:  bool
}

constraints.cue

import "strings"

album: {
	title: strings.MinRunes(5)
	year:  >1950
	live:  false
}

data.cue

album: {
	title: "Houses of the Holy"
	year:  1973
	live:  false
}

While you’re likely familiar with types and data, most languages do not make constraints or validation a first-class concept. Cue does, with constraints, which place rules or restrictions on values. Like all instances, they live in the value lattice between schemas and fully specified values.

Cue’s best practice has us start with open schemas, limit possibilities in context, and eventually arrive at a concrete instances. You’ll also want to start with small schemas and build them up into more complex instances.

Order is Irrelevant

Cue’s unification system resolves values, schemas, and constraints regardless of order and which files may contain them.

order.cue

// you can add constraints after
a: 3
a: int
a: >1

// define a struct in one place
s: {
	x: int
	y: int
}

// define a struct in parts
s: y: int
s: x: int

// the above is shorthand
// when setting a nested value

More generally, unification is associative, commutative and idempotent.

Values Cannot Be Changed

One of the most important aspects of Cue to understand is that values cannot be changed. There are no overloads or overrides in Cue. This has implications on how you write and organize code. The reason is for maintainability and comprehension, but is also required by Cue’s philosophy. You will find this useful if you’ve ever wondered where else some value in your configuration was set from. Cue will not only guarantee that it is the value you set it to, it will also tell you where it was set and other locations if there is a conflict.

immutable.cue

// cannot change concrete values       
a: "a"
a: "b"

// cannot change type values
n: int
n: string

cue eval immutable.cue

a: conflicting values "b" and "a":
    ./immutable.cue:2:4
    ./immutable.cue:3:4
n: conflicting values int and string (mismatched types int and string):
    ./immutable.cue:6:4
    ./immutable.cue:7:4

Defining Fields

Cue allows fields to be defined more than once as long as they are consistent with each other.

  • basic data types must be the same
  • you can make a field more restrictive, but not the other way
  • structs fields are merged, list elements must match exactly
  • the rules are applied recursively

fields.cue

hello: "world"
hello: "world"

// set a type
s: {a: int}

// set some data
s: {a: 1, b: 2}

// set a nested field without curly braces
s: c: d: 3

// lists must have the same elements
// and cannot change length
l: ["abc", "123"]
l: [
	"abc",
	"123",
]

cue eval fields.cue

hello: "world"
s: {
	a: 1
	c: {
		d: 3
	}
	b: 2
}
l: ["abc", "123"]

Using these properties will be useful for defining schemas in one place, constraints in another, building up configuration, and ensuring that a field was not incorrectly set to different values in different places.

Definitions

Definitions are Cue’s way of specifying schemas. They have slightly different rules from structs.

  • They are not output as data
  • They may remain incomplete or under specified
  • They “close” a struct, forbidding unknown or additional fields

You indicate a definitions with #mydef: and can leave it open with ...

definition.cue

#Album: {
	artist: string
	title:  string
	year:   int

	// ...  // 2. uncomment to open and fix error, must be last
}

// This is a conjunction, it says "album" has to be "#Album"
album: #Album & {
	artist: "Led Zeppelin"
	title:  "Led Zeppelin I"
	year:   1969

	// studio: true  // 1. uncomment to trigger error
}

cue export definition.cue --out json

{
    "album": {
        "artist": "Led Zeppelin",
        "title": "Led Zeppelin I",
        "year": 1969
    }
}

Conjunctions

Conjunctions “meet” values together, combining their fields, rules, and data. They are like “and” and the & operator is used for them.

conjunction.cue

// conjunctions on a field
n: int & >0 & <100
n: 23

// conjunction of schemas
val: #Def1 & #Def2
val: {foo: "bar", ans: 42}

#Def1: {
	foo: string
	ans: int
}

#Def2: {
	foo: =~"[a-z]+"
	ans: >0
}

cue export conjunction.cue --out json

{
    "n": 23,
    "val": {
        "foo": "bar",
        "ans": 42
    }
}

Disjunctions

Disjunctions “join” values to create options or alternatives. They are like “or” and the | operator is used for them.

disjunction.cue

	// disjunction of values (like an enum)
hello: "world" | "bob" | "mary"
hello: "world"

// disjunction of types
port: string | int
port: 5432

// disjunction of schemas
val: #Def1 | #Def2
val: {foo: "bar", ans: 42}

#Def1: {
	foo: string
	ans: int
}

#Def2: {
	name: string
	port: int
}

cue export disjunction.cue --out json

{
    "hello": "world",
    "port": 5432,
    "val": {
        "foo": "bar",
        "ans": 42
    }
}

Disjunctions have several uses:

  • enums (as values)
  • sum-type (any of these types)
  • null-coalescing (use this computation, or default to some value)

Required, Optionals, and Defaults

Cue supports marking a field as optional, required, or providing a default.

default-optional.cue

s: {
	// field with a default
	hello: string | *"world" | "apple"
	// an optional integer
	count?: int

	// uncomment to cause failure when not supplied
	// needed!: _
}

cue export default-optional.cue --out json

{
    "s": {
        "hello": "world"
    }
}

We are currently in a transition from optional to required markers.

  • before, optional had to be marked, required was the default
  • future, required will be maredk and optional will be the default

Incomplete and Concrete

An incomplete value is one which does not have all fields filled with data. Cue will not export incomplete values and instead return an error. By contrast, concrete is a fully specified value.

incomplete.cue

// incomplete values
a: _
b: int

s: {
  a: _
}

// concrete values
a: "a"
b: int

s: a: { foo: "bar" }

Open and Closed

Open means a struct can be extended, closed means they cannot. By default, structs are open and definitions are closed. Cue also allows us to explicitly do the opposite.

open-closed.cue

	// Closed struct
s: close({
	foo: "bar"
})

// Open definition
#d: {
	foo: "bar"
	... // must be last
}

For a more in-depth discussion, see deep-dives/closedness.

Building Up Values

In Cue, it is recommended to start small and build values up. This makes schemas reusable. You can do this by embedding values.

building-up.cue

#Base: {
	name: string
	kind: string
}

#Meta: {
	// string and a semver regex
	version: string & =~"^v[0-9]+\\.[0-9]+\\.[0-9]+$"
	// list of strings
	labels: [...string]
}

#Permissions: {
	role:   string
	public: bool | *false
}

// Building up a schema using embeddings
#Schema: {
	// embed other schemas
	#Base
	#Meta

	#Permissions
	// with no '...' this is final
}

value: #Schema & {
	name:    "app"
	kind:    "deploy"
	version: "v1.0.42"
	labels: ["server", "prod"]
	role: "backend"
	// public: false  (by default)
}

cue export building-up.cue --out json

{
    "value": {
        "name": "app",
        "kind": "deploy",
        "version": "v1.0.42",
        "labels": [
            "server",
            "prod"
        ],
        "role": "backend",
        "public": false
    }
}

Turing-Incomplete

Cue is Turing-incomplete, meaning you will not program like typical languages. Rather, you will provide values, types, definitions, and constraints; and Cue will tell you if what you have written is correct. This choice is intentional and based on years of experience managing configuration at Google.

The main ideas are:

  • wrap code in data, not data in code
  • no primitive recursion or inheritance
  • the initial learning curve is worth the long-term maintenance

The main inspirations for these restrictions are:

  • Difficulties with Borgcfg and GCL as complexity grew (i.e. object oriented and lambdas)
  • Lingo and Typed Feature Structure Grammars (managing massive configurations)
  • Logical and functional languages (various pieces like comprehensions in immutability)

Foundations in Golang

Cue started as a fork of Go mainly to simplify the bootstrapping of a new language. Marcel was also a member of the Go team at Google and many philosophies carry over:

  • Cue is implemented in Go
  • Rich tooling in an awesome CLI
  • APIs for working with the language
  • A standard library included
We'll never share your email with anyone else.
2024 Hofstadter, Inc