Open and Closedness



Closedness describes the state of a value in terms of extensibility. In the simplest terms, a closed value cannot have fields added and an open value can. There are a lot of rules for determining a Value’s closed state and some constructs define sets of fields. So don’t worry if it seems confusing at first, the implementation is even trickier!


Structs are open by default

Structs are open and extensible by default. You can add fields through or after conjunction. This applies recursively to all fields.

struct.cue

S: {
	name: string
	point: {
		x: int
		y: int
	}
}

// we can extend the fields
s: S & {
	data: bytes
	point: z: int
}

Definitions are closed by default

Definitions have a fixed structure. You can be sure no extra fields will be added. This applies recursively to all fields.

definition.cue

#D: {
	name: string
	data: {
		num: int
	}
}

// you can split the spec, this is not extension
#D: tags: [...string]

// after conjuncting, extension is not allowed
d: #D & {
	meta: string
	data: {
		val: string
	}
}

Opening and closing values

You can open definitions with ... and close structs with close. These are not recursively applied, only changing the field they are used on.

open-n-close.cue

S: close({
	name: string
	point: {
		x: int
		y: int
	}
})

s: S & {
	// this is no longer allowed
	data: bytes
	// this is still allowed
	point: z: int
}

#D: {
	name: string
	data: {
		num: int
		...
	}
	...
}

// this is now allowed
d: #D & {
	meta: string
	data: {
		val: string
	}
}

Closedness with pattern constraints

Pattern constraints define a set of values. So while d & #D is closed, it can still have an infinite number of labels defined.

pattern-constraints.cue

#D: {
	labels: [=~"dev"]: string
}

d: #D & {
	labels: {
		// allowed
		"devUser": "foo"
		// not allowed
		"appUser": "bar"
	}
}

Extending definitions by embedding

Embedding allows the extension of a definition while still receiving updates when #D is changed.

embed.cue

#D: {
	name: string
	data: {
		num: int
	}
}

// embed D into a new definition
#L: {
	#D
	tags: [...string]
}

// after conjuncting, extension is not allowed
d: #L & {
	meta: "meta"
	data: {
		num: 3
	}
	tags: ["foo", "bar"]
}

Hidden fields

You can add hidden fields to a closed value. This works for both definitions and structs which have been close()’d.

hidden.cue

#D: {
	size: string
	data: {
		x: int
		y: int
	}
}

// after conjuncting, extension is not allowed
d: #D & {
	data: {
		x: 3
		y: 4
	}

	_calc: data.x * data.y
	size:  string | *"med"
	if _calc < 10 {
		size: "small"
	}
	if _calc > 100 {
		size: "large"
	}
}

List closedness

List open and closedness is much simpler than structs and definitions. When you use ellipses, the list is open. Any fixed elements are required in the exact position.

list.cue

// an open list
L1: [...]

// one element list with any type
L2: [_]

// an int list with at least one element
L3: [int, ...]

// a mixed list of four elements
L4: [int, string, {...}, _]

// you can concatenate lists too
L5: L2 + L3 // open
L6: L2 + L4 // closed
L7: L3 + L2 // closed, L3 ellipses removed

// You cannot append or reopen like this
L2: L2 + [...]
We'll never share your email with anyone else.
2024 Hofstadter, Inc