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

{
    "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. 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.

Schema

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

Constraints

album: {
	title: string
	year: >1950
	live: false
}

Data

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.

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.

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

	// ...  uncomment to open, 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  (uncomment to trigger error)
}

cue export definition.cue

{
    "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

// conjuction 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

{
    "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

{
    "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)

Defaults and Optionals

Cue supports setting defaults for values or marking a field optional.

default-optional.cue

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

cue export default-optional.cue

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

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.

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
}

Building Up Values

In Cue, it is recommended to start small and build values up. This makes schemas reusable.

building-up.cue

#Base: {
	name: string
	kind: string
	... // so it can be extended
}
#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 a conjunction and embedding
#Schema: #Base & {
	// "embed" meta and permissions
	#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

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

Order is Irrelevant

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

order.cue

a: 3
b: "bb"

// notice the mixing
s: { x: 1,   y: int }
s: { x: int, y: 2 }

cue export order.cue order-2.cue

{
    "a": 3,
    "b": "bb",
    "s": {
        "x": 1,
        "y": 2
    }
}

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 orientedness)
  • 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 is 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
Last modified April 2, 2021: foundations.md#Disjunctions: typo (57c9261)

2021 Hofstadter, Inc