References provide two features.
First, they allow users to convey expressions in the target format using plain expressions in
Pkl itself.
Second, they provide type safety:
- Pkl will typecheck a reference's domain and referent type.
- Dot access and subscript access is checked.
Instances are either created through the Reference() constructor method,
or synthetically through member access.
References consist of four parts:
- domain: determines which references are compatible and how references are
rendered as strings.
- data: an arbitrary value that may contain domain-specific information about
the referenced value.
- path: a
List<Access> of values indicating how the reference was accessed
(by property or subscript).
- Referent type: the type of the value that the reference refers to.
This type is internally held, and not exposed as an in-language value.
Member access
A reference contains synthetic members.
These synthetic members may be accessed using the dot and subscript operators, and are
generated when the member access itself is executed.
myRef.name
myRef["bar"]
Members are synthesized based on the referent type:
Given the following declaration:
class Bird { name: String }
A Reference<MyDomain, Bird> will contain synthetic property
name: Reference<MyDomain, String>.
The synthesized reference contains the same domain and data as the
original reference.
This reference's path extends the original reference's with an Access instance describing
the accessed property name or subscript key.
Limitations
- Properties with the
external modifier may not be accessed.
- Properties defined inside external classes may not be accessed.
- The
Listing.default, Mapping.default, and Dynamic.default properties may not be
accessed.
- Constrained types cannot be referent types.
- Declaring
Reference<MyDomain, String(length.isOdd)> will throw an error.
- Member access will return a new reference whose referent type has its constraints erased.
Domain
The Domain parameter identifies the system that this reference exists inside.
Additionally, it defines how references should be rendered when Pkl produces textual output.
Example:
import "pkl:ref"
class MyDomain extends ref.Domain {
// Define how this domain renders references to strings:
function renderReference(reference: ref.Reference<MyDomain, Any>): String =
reference.getData().toString()
+ "/"
+ reference.getPath().map((it) -> it.property ?? it.key.toString()).join("/")
}
A typealias can be used to improve ergonomics:
typealias MyReference<T> = ref.Reference<MyDomain, T>
Example
import "pkl:ref"
class Foo extends ref.Domain {
function renderReference(reference: ref.Reference<Foo, Any>) =
"{{ "
+ reference.getData().toString()
+ reference
.getPath()
.map((it) -> if (it.isProperty) "/\(it.property)" else "[\(it.key)]")
.join("")
+ " }}"
}
typealias FooReference<T> = ref.Reference<Foo, T>
class Outputs {
file: String
properties: Mapping<String, String>
}
local outputs: FooReference<Outputs> = ref.Reference(new Foo {}, Outputs, "outputs")
// Dot access; gives a `Reference<Foo, String>`
myFile = outputs.file
// Subscript access; gives a `Reference<Foo, String>`
myProperty = outputs.properties["myProperty"]
output {
renderer {
converters {
[ref.Reference] = (it) -> it.toString()
}
}
}
Output:
myFile = "{{ outputs/file }}"
myProperty = "{{ outputs/properties[myProperty] }}"
References are an advanced API design tool that enables library authors to express domain-specific values, whose actual underlying values are not known to Pkl during evaluation.