cty
is built around two fundamental concepts: types and values.
A type represents a particular way of storing some data in memory and a set of operations that can be performed on that data. A value is, therefore, the combination of some raw data and a type that describes how that data should be interpreted.
The simplest types in cty
are the number, string and bool types,
collectively known as the primitive types.
Along with the primitive types, cty
supports compound types, which are
types that are constructed by assembling together other types in a particular
way. The compound types are further subdivided into two categories:
-
Collection Types represent collections of values that all have the same type (the element type) and permit access to those values in different ways. The collection type kinds are list, set, and map.
-
Structural Types represent collections of values that may all have different types, organized either by name or by position in a sequence. The structural type kinds are object and tuple.
For example, "list of string" is a collection type that represents a collection of string values (elements) that are each assigned a sequential index starting at zero, while "map of string" instead assigns each of its elements a name in the form of a string value.
The details of the specific types and type kinds are covered in the full description of the type system; the remainder of this document will discuss types and values in general, using specific types only as examples.
Each type defines a set of operations that are valid on its values. For example, the number type permits various arithmetic operations such as addition, subtraction, and multiplication, but these are not permitted for other types such as bool.
Since cty
is a dynamic type system (from the perspective of the calling Go
program), the validity of an operation on a given value must be checked at
runtime. The documentation for each type defines what operations are valid
on it and what semantics each operation has.
cty
has some additional optional concepts that may be useful in certain
applications.
An unknown value is a value that carries a type but no value. It can serve as a placeholder for a value to be resolved later, which can be useful when implementing a static type checker for a language. Unknown values are special because they support the same operations as a known value of the same type but the result will itself be an unknown value. For example, the number 5 added to an unknown number yields another unknown number.
The dynamic pseudo-type is a special type that serves as a placeholder for a type that isn't yet known. Whereas unknown values represent situations where the type is known and the value is not, the dynamic pseudo-type represents situations where neither is known, or where any value of any type is permitted. It is referred to as a "pseudo-type" because while it can be used in many places where types are permitted, it does not define any operations of its own.
These two concepts are related in that the dynamic pseudo-type has no non-null, non-unknown values. It single non-null type is itself an unknown value. All operations are supported on non-null dynamic values, but the result will always be an unknown value, possibly type-unknown itself.
Dealing with unknown values and the dynamic pseudo-type can cause additional
complexity for a calling application, although many details of it are handled
automatically by the cty
internals. As a consequence, the main cty
API
promises to never produce an unknown value for an operation unless one of the
operands is itself unknown, and so applications can opt out of this additional
complexity by never providing unknown values as operands.
At minimum an unknown value has a type constraint which describes a set of types that the final value could possibly have once known. In some cases we can refine an unknown value with additional dynamic information, using Value Refinements.
Two types are said to be equal if they are exactly equivalent. Each type kind defines its own equality rules, but the overall intent is to implement strict type comparisons.
Type conformance is a slightly-weaker concept that allows the dynamic pseudo-type to be used as a placeholder to represent "any type". Therefore a given type is equal only to itself but it is conformant to either itself or the dynamic pseudo-type.
Type conformance is not directly used by cty
's core, but it is used as
a building block for the function
package and for JSON serialization.
The primary way a application works with cty
values is via the API exposed
by the cty
go package. The full details of this package are in
its reference documentation,
so this section will just cover the basic usage patterns.
The main features of the cty
package are the Go types cty.Type
and cty.Value
,
which each represent the concept they are named after.
The package contains variables that represent the primitive types, cty.Number
,
cty.String
and cty.Bool
. It also contains functions that allow the
construction of compound types, such as cty.List
, cty.Object
, etc. These
functions each take different arguments depending on the kind of compound type
in question.
Alongside the types and type factories, the package also contains variables
and functions for constructing values of these types, which conventionally
have names that are the corresponding type or type kind with the suffix Val
.
For example, the two boolean values are exported as cty.True
and cty.False
,
and string values can be constructed using the function cty.StringVal
, given
a native Go string.
The cty.Type
and cty.Value
types are similar to the types of the same
name in the built-in Go reflect
package. They expose methods that are the
union of all operations supported across all types, but each method has a
set of constraints associated with it, and failure to follow these will result
in a run-time panic.
The cty.Value
object has two classes of methods:
-
Operation Methods stay within the
cty
type system, dealing entirely withcty.Value
instances. These methods fully deal with concerns such as unknown values, so the caller just needs to be sure to apply only operations that are valid for the receiving value's type. -
Integration Methods live on the boundary between
cty
and the native Go type system, and can be used by the calling application to integrate with non-cty
-aware code. These methods often have constraints such as not supporting unknown values, which are covered in their documentation.
While the integration methods alone are sufficient for a calling application
to convert to and from cty
values, the utility package
gocty
provides a more convenient way to convert between
Go native values and cty
values.
A cty.Value
can optionally be marked, which causes it to carry around some
additonal metadata along with its value. Marks are just normal Go values that
are value to use as map keys, and are compared by equality.
For example, an application might use marks to track the origin of a particular value in order to give better error messages, or to present the value in a different way in a UI.
When a value is marked, operation methods using that value will propagate the marks to any result values. That makes marks "infectious" in the sense that they propagate through operations and accumulate in the result automatically.
For more information on marks, see the dedicated section on marks.