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
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.
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.
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
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 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
Conjunctions “meet” values together, combining their fields, rules, and data.
They are like “and” and the
& operator is used for them.
Disjunctions “join” values to create options or alternatives.
They are like “or” and the
| operator is used for them.
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.
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.
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.
Order is Irrelevant
Cue’s unification system resolves values, schemas, and constraints regardless of order and which files may contain them.
More generally, unification is associative, commutative and idempotent.
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