Files
pkl/docs/modules/language-reference/pages/index.adoc
Jen Basch 9427387019 Add release notes for 0.30 (#1261)
Co-authored-by: Dan Chao <dan.chao@apple.com>
2025-10-30 10:09:35 -07:00

5784 lines
163 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
= Language Reference
include::ROOT:partial$component-attributes.adoc[]
:uri-common-mark: https://commonmark.org/
:uri-newspeak: https://newspeaklanguage.org
:uri-prototypical-inheritance: https://en.wikipedia.org/wiki/Prototype-based_programming
:uri-double-precision: https://en.wikipedia.org/wiki/Double-precision_floating-point_format
:uri-progressive-disclosure: https://en.wikipedia.org/wiki/Progressive_disclosure
:uri-javadoc-Pattern: https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html
:uri-github-PklLexer: {uri-github-tree}/pkl-core/src/main/java/org/pkl/core/parser/Lexer.java
:uri-pkl-core-ModuleSchema: {uri-pkl-core-main-sources}/ModuleSchema.java
:uri-pkl-core-SecurityManager: {uri-pkl-core-main-sources}/SecurityManager.java
:uri-pkl-core-ResourceReader: {uri-pkl-core-main-sources}/resource/ResourceReader.java
:uri-pkl-core-ModuleKey: {uri-pkl-core-main-sources}/module/ModuleKey.java
:uri-pkl-core-PklException: {uri-pkl-core-main-sources}/PklException.java
:uri-value-converters: {uri-pkl-stdlib-docs}/base/PcfRenderer#converters
// TODO: double check this
:uri-pkl-go-resource-reader-docs: https://github.com/apple/pkl-go/blob/main/pkl/reader.go
:uri-pkl-swift-resource-reader-docs: https://github.com/apple/pkl-swift/blob/main/Sources/PklSwift/Reader.swift
:uri-glob-7: https://man7.org/linux/man-pages/man7/glob.7.html
:uri-unicode-identifier: https://unicode.org/reports/tr31/#R1-1
:uri-semver: https://semver.org
:uri-mvs-build-list: https://research.swtch.com/vgo-mvs#algorithm_1
The language reference provides a comprehensive description of every Pkl language feature.
<<comments,Comments>> +
<<numbers,Numbers>> +
<<booleans,Booleans>> +
<<strings,Strings>> +
<<durations,Durations>> +
<<data-sizes,Data Sizes>> +
<<objects,Objects>> +
<<listings,Listings>> +
<<mappings,Mappings>> +
<<classes,Classes>> +
<<methods,Methods>> +
<<modules,Modules>> +
<<null-values,Null Values>> +
<<if-expressions,If Expressions>> +
<<resources,Resources>> +
<<errors,Errors>> +
<<debugging,Debugging>> +
<<advanced-topics,Advanced Topics>>
For a hands-on introduction, see xref:language-tutorial:index.adoc[Tutorial].
For ready-to-go examples with full source code, see xref:ROOT:examples.adoc[].
For API documentation, see xref:ROOT:standard-library.adoc[Standard Library].
[[comments]]
== Comments
Pkl has three forms of comments:
Line comment::
A code comment that starts with a double-slash (`//`) and runs until the end of the line.
+
[source%parsed,{pkl}]
----
// Single-line comment
----
Block comment::
A nestable multiline comment, which is typically used to comment out code.
Starts with `+/*+` and ends with `+*/+`.
+
[source%parsed,{pkl}]
----
/*
Multiline
comment
*/
----
Doc comment::
A user-facing comment attached to a program member.
It starts with a triple-slash (`///`) and runs until the end of the line.
Doc comments on consecutive lines are merged.
+
[source%parsed,{pkl}]
----
/// A *bird* superstar.
/// Unfortunately, extinct.
dodo: Bird
----
Doc comments are processed by xref:pkl-doc:index.adoc[Pkldoc], Pkl's documentation generator.
For details on their syntax, see <<doc-comments,Doc Comments>>.
[[numbers]]
== Numbers
Pkl has two numeric types, link:{uri-stdlib-Int}[Int] and link:{uri-stdlib-Float}[Float].
Their common supertype is link:{uri-stdlib-Number}[Number].
=== Integers
A value of type link:{uri-stdlib-Int}[Int] is a 64-bit signed integer.
Integer literals can be written in decimal, hexadecimal, binary, or octal notation:
[source%tested,{pkl}]
----
num1 = 123
num2 = 0x012AFF // <1>
num3 = 0b00010111 // <2>
num4 = 0o755 // <3>
----
<1> decimal: 76543
<2> decimal: 23
<3> decimal: 493
Integers can optionally include an underscore as a separator to improve readability.
An underscore does not affect the integer's value.
[source%tested,{pkl}]
----
num1 = 1_000_000 // <1>
num2 = 0x0134_64DE // <2>
num3 = 0b0001_0111 // <3>
num4 = 0o0134_6475 // <4>
----
<1> Equivalent to `1000000`
<2> Equivalent to `0x013464DE`
<3> Equivalent to `0b00010111`
<4> Equivalent to `0o01346475`
Negative integer literals start with a minus sign, as in `-123`.
Integers support the standard comparison operators:
[source%tested,{pkl}]
----
comparison1 = 5 == 2
comparison2 = 5 < 2
comparison3 = 5 > 2
comparison4 = 5 <= 2
comparison5 = 5 >= 2
----
Integers support the following arithmetic operators:
[source%tested,{pkl}]
----
num1 = 5 + 2 // <1>
num2 = 5 - 2 // <2>
num3 = 5 * 2 // <3>
num4 = 5 / 2 // <4>
num5 = 5 ~/ 2 // <5>
num6 = 5 % 2 // <6>
num7 = 5 ** 2 // <7>
----
<1> addition (result: `7`)
<2> subtraction (result: `3`)
<3> multiplication (result: `10`)
<4> division (result: `2.5`, always `Float`)
<5> integer division (result: `2`, always `Int`)
<6> remainder (result: `1`)
<7> exponentiation (result: `25`)
Arithmetic overflows are caught and result in an error.
To restrict an integer's range, use one of the predefined <<predefined-type-aliases,type aliases>>
or an link:{uri-stdlib-isBetween}[isBetween] <<type-constraints,type constraint>>:
[source,{pkl}]
----
clientPort: UInt16
serverPort: Int(isBetween(0, 1023))
----
=== Floats
A value of type link:{uri-stdlib-Float}[Float] is a 64-bit link:{uri-double-precision}[double-precision] floating point number.
Float literals use decimal notation.
They consist of an integer part, decimal point, fractional part, and exponent part.
The integer and exponent parts are optional.
[source%tested,{pkl}]
----
num1 = .23
num2 = 1.23
num3 = 1.23e2 // <1>
num4 = 1.23e-2 // <2>
----
<1> result: 1.23 * 10^2^
<2> result: 1.23 * 10^-2^
Negative float literals start with a minus sign, as in `-1.23`.
The special float values _not a number_, _positive infinity_, and _negative infinity_ are written as:
[source%tested,{pkl}]
----
notANumber = NaN
positiveInfinity = Infinity
negativeInfinity = -Infinity
----
The link:{uri-stdlib-NaN}[NaN] and link:{uri-stdlib-Infinity}[Infinity] properties are defined in the standard library.
Floats support the same comparison and arithmetic operators as integers.
Float literals with a fractional part of zero can be safely replaced with integer literals.
For example, it is safe to write `1.3 * 42` instead of `1.3 * 42.0`.
Floats can also include the same underscore separator as integers. For example, `1_000.4_400` is a float whose value is equivalent to `1000.4400`.
TIP: As integers are more convenient to use than floats with a fractional part of zero, we recommend requiring `x: Number` instead of `x: Float` in type annotations.
To restrict a float to a finite value, use the link:{uri-stdlib-isFinite}[isFinite] <<type-constraints,type constraint>>:
[source,{pkl}]
----
x: Float(isFinite)
----
To restrict a float's range, use the link:{uri-stdlib-isBetween}[isBetween] type constraint:
[source,{pkl}]
----
x: Float(isBetween(0, 10e6))
----
[[booleans]]
== Booleans
A value of type link:{uri-stdlib-Boolean}[Boolean] is either `true` or `false`.
Apart from the standard logical operators, `Boolean` has
link:{uri-stdlib-xor}[xor] and link:{uri-stdlib-implies}[implies] methods.
[source%tested,{pkl}]
----
res1 = true && false // <1>
res2 = true || false // <2>
res3 = !false // <3>
res4 = true.xor(false) // <4>
res5 = true.implies(false) // <5>
----
<1> logical conjunction (result: `false`)
<2> logical disjunction (result: `true`)
<3> logical negation (result: `true`)
<4> exclusive disjunction (result: `true`)
<5> logical implication (result: `false`)
[[strings]]
== Strings
A value of type link:{uri-stdlib-String}[String] is a sequence of Unicode code points.
String literals are enclosed in double quotes:
[source%tested,{pkl-expr}]
----
"Hello, World!"
----
TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences,
have stricter rules for line indentation in multiline strings, and do not have a line continuation character.],
String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them!
Inside a string literal, the following character escape sequences have special meaning:
* `\t` - tab
* `\n` - line feed
* `\r` - carriage return
* `\"` - verbatim quote
* `\\` - verbatim backslash
Unicode escape sequences have the form `\u{<codePoint>}`, where `<codePoint>` is a hexadecimal number between 0 and 10FFFF:
[source%tested,{pkl-expr}]
----
"\u{26} \u{E9} \u{1F600}" // <1>
----
<1> result: `"& é 😀"`
To concatenate strings, use the `+` (plus) operator, as in `"abc" + "def" + "ghi"`.
=== String Interpolation
To embed the result of expression `<expr>` in a string, use `\(<expr>)`:
[source%tested,{pkl}]
----
name = "Dodo"
greeting = "Hi, \(name)!" // <1>
----
<1> result: `"Hi, Dodo!"`
Before a result is inserted, it is converted to a string:
[source%tested,{pkl}]
----
x = 42
str = "\(x + 2) plus \(x * 2) is \(0x80)" // <1>
----
<1> result: `"44 plus 84 is 128"`
=== Multiline Strings
To write a string that spans multiple lines, use a multiline string literal:
[source%tested,{pkl-expr}]
----
"""
Although the Dodo is extinct,
the species will be remembered.
"""
----
Multiline string literals are delimited by three double quotes (`"""`).
String content and closing delimiter must each start on a new line.
The content of a multiline string starts on the first line after the opening quotes and ends on the last line before the closing quotes.
Line breaks are included in the string and normalized to `\n`.
The previous multiline string is equivalent to this single-line string.
Notice that there is no leading or trailing whitespace.
[source%tested,{pkl-expr}]
----
"Although the Dodo is extinct,\nthe species will be remembered."
----
String interpolation, character escape sequences, and Unicode escape sequences work the same as for single-line strings:
[source%tested,{pkl}]
----
bird = "Dodo"
message = """
Although the \(bird) is extinct,
the species will be remembered.
"""
----
Each content line must begin with the same whitespace characters as the line containing the closing delimiter,
which is not included in the string. Any further leading whitespace characters are preserved.
In other words, line indentation is controlled by indenting lines relative to the closing delimiter.
In the following string, lines have no leading whitespace:
[source%tested,{pkl}]
----
str = """
Although the Dodo
is extinct,
the species
will be remembered.
"""
----
In the following string, lines are indented between three and five spaces:
[source%tested,{pkl}]
----
str = """
Although the Dodo
is extinct,
the species
will be remembered.
"""
----
[[custom-string-delimiters]]
=== Custom String Delimiters
Some strings contain many verbatim backslash (`\`) or quote (`"`) characters.
A good example is regular expressions, which make frequent use of backslash characters for escaping.
In such cases, using the escape sequences `\\` and `\"` quickly becomes tedious and hampers readability.
Instead, leading/closing string delimiters can be customized to start/end with a pound sign (`\#`).
This also affects the escape character, which changes from `\` to `\#`.
All backslash and quote characters in the following string are interpreted verbatim:
[source%tested,{pkl-expr}]
----
#" \\\\\ """"" "#
----
Escape sequences and string interpolation still work, and now start with `\#`:
[source%tested,{pkl}]
----
bird = "Dodo"
str = #" \\\\\ \#n \#u{12AF} \#(bird) """"" "#
----
More generally, string delimiters and escape characters can be customized to contain _n_ pound signs each, for n >= 1.
In the following string, _n_ is 2. As a result, the string content is interpreted verbatim:
[source%tested,{pkl-expr}]
----
##" \\\\\ \#\#\# """"" "##
----
=== String API
The `String` class offers a link:{uri-stdlib-String}[rich API].
Here are just a few examples:
[source%tested,{pkl}]
----
strLength = "dodo".length // <1>
reversedStr = "dodo".reverse() // <2>
hasAx = "dodo".contains("alive") // <3>
trimmed = " dodo ".trim() // <4>
----
<1> result: `4`
<2> result: `"odod"`
<3> result: `false`
<4> result: `"dodo"`
[[durations]]
== Durations
A value of type link:{uri-stdlib-Duration}[Duration] has a _value_ component of type `Number` and a _unit_ component of type `String`.
The unit component is constrained to the units defined in link:{uri-stdlib-DurationUnit}[DurationUnit].
Durations are constructed with the following `Number` properties:
[source%tested,{pkl}]
----
duration1 = 5.ns // nanoseconds (smallest unit)
duration2 = 5.us // microseconds
duration3 = 5.ms // milliseconds
duration4 = 5.s // seconds
duration5 = 5.min // minutes
duration6 = 5.h // hours
duration7 = 3.d // days (largest unit)
----
A duration can be negative, as in `-5.min`.
It can have a floating point value, as in `5.13.min`.
The link:{uri-stdlib-Duration-value}[value] and link:{uri-stdlib-Duration-unit}[unit] properties provide access to the duration's components.
Durations support the standard comparison operators:
[source%tested,{pkl}]
----
comparison1 = 5.min == 3.s
comparison2 = 5.min < 3.s
comparison3 = 5.min > 3.s
comparison4 = 5.min <= 3.s
comparison5 = 5.min >= 3.s
----
Durations support the same arithmetic operators as numbers:
[source%tested,{pkl}]
----
res1 = 5.min + 3.s // <1>
res2 = 5.min - 3.s // <2>
res3 = 5.min * 3 // <3>
res4 = 5.min / 3 // <4>
res5 = 5.min / 3.min // <5>
res6 = 5.min ~/ 3 // <6>
res7 = 5.min ~/ 3.min // <7>
res8 = 5.min % 3 // <8>
res9 = 5.min ** 3 // <9>
----
<1> result: `5.05.min`
<2> result: `4.95.min`
<3> result: `15.min`
<4> result: `1.6666666666666667.min`
<5> result: `1.6666666666666667`
<6> result: `1.min`
<7> result: `1`
<8> result: `2.min`
<9> result: `125.min`
The value component can be an expression:
[source%tested,{pkl}]
----
x = 5
xMinutes = x.min // <1>
y = 3
xySeconds = (x + y).s // <2>
----
<1> result: `5.min`
<2> result: `8.s`
[[data-sizes]]
== Data Sizes
A value of type link:{uri-stdlib-DataSize}[DataSize] has a _value_ component of type `Number` and a _unit_ component of type `String`.
The unit component is constrained to the units defined in link:{uri-stdlib-DataSizeUnit}[DataSizeUnit].
Data sizes with decimal units (factor 1000) are constructed with the following `Number` properties:
[source%tested,{pkl}]
----
datasize1 = 5.b // bytes (smallest unit)
datasize2 = 5.kb // kilobytes
datasize3 = 5.mb // megabytes
datasize4 = 5.gb // gigabytes
datasize5 = 5.tb // terabytes
datasize6 = 5.pb // petabytes (largest unit)
----
Data sizes with binary units (factor 1024) are constructed with the following `Number` properties:
[source%tested,{pkl}]
----
datasize1 = 5.b // bytes (smallest unit)
datasize2 = 5.kib // kibibytes
datasize3 = 5.mib // mebibytes
datasize4 = 5.gib // gibibytes
datasize5 = 5.tib // tebibytes
datasize6 = 5.pib // pebibytes (largest unit)
----
A data size can be negative, as in `-5.mb`.
It can have a floating point value, as in `5.13.mb`.
The link:{uri-stdlib-DataSize-value}[value] and link:{uri-stdlib-DataSize-unit}[unit] properties provide access to the data size's components.
Data sizes support the standard comparison operators:
[source%tested,{pkl}]
----
comparison1 = 5.mb == 3.kib
comparison2 = 5.mb < 3.kib
comparison3 = 5.mb > 3.kib
comparison4 = 5.mb <= 3.kib
comparison5 = 5.mb >= 3.kib
----
Data sizes support the same arithmetic operators as numbers:
[source%tested,{pkl}]
----
res1 = 5.mb + 3.kib // <1>
res2 = 5.mb - 3.kib // <2>
res3 = 5.mb * 3 // <3>
res4 = 5.mb / 3 // <4>
res5 = 5.mb / 3.mb // <5>
res6 = 5.mb ~/ 3 // <6>
res7 = 5.mb ~/ 3.mb // <7>
res8 = 5.mb % 3 // <8>
res9 = 5.mb ** 3 // <9>
----
<1> result: `5.003072.mb`
<2> result: `4.996928.mb`
<3> result: `15.mb`
<4> result: `1.6666666666666667.mb`
<5> result: `1.6666666666666667`
<6> result: `1.mb`
<7> result: `1`
<8> result: `2.mb`
<9> result: `125.mb`
The value component can be an expression:
[source%tested,{pkl}]
----
x = 5
xMegabytes = x.mb // <1>
y = 3
xyKibibytes = (x + y).kib // <2>
----
<1> result: `5.mb`
<2> result: `8.kib`
[[objects]]
== Objects
An object is an ordered collection of _values_ indexed by _name_.
An object's key-value pairs are called its _properties_.
Property values are lazily evaluated on the first read.
Because Pkl's objects differ in important ways from objects in general-purpose programming languages,
and because they are the backbone of most data models, understanding objects is the key to understanding Pkl.
[[defining-objects]]
=== Defining Objects
Let's define an object with properties `name` and `extinct`:
[source%tested,{pkl}]
----
dodo { // <1>
name = "Dodo" // <2>
extinct = true // <3>
} // <4>
----
<1> Defines a module property named `dodo`.
The open curly brace (`{`) indicates that the value of this property is an object.
<2> Defines an object property named `name` with string value `"Dodo"`.
<3> Defines an object property named `extinct` with boolean value `true`.
<4> The closing curly brace indicates the end of the object definition.
To access an object property by name, use dot (`.`) notation:
[source%tested,{pkl}]
----
dodoName = dodo.name
dodoIsExtinct = dodo.extinct
----
Objects can be nested:
[source%tested,{pkl}]
----
dodo {
name = "Dodo"
taxonomy { // <1>
`class` = "Aves" // <2>
}
}
----
<1> Defines an object property named `taxonomy`.
The open curly brace indicates that its value is another object.
<2> The word `class` is a keyword of Pkl, and needs to be wrapped in backticks (```) to be used as a property.
As you probably guessed, the nested property `class` can be accessed with `dodo.taxonomy.class`.
Like all values, objects are _immutable_, which is just a fancy (and short!) way to say that their properties never change.
So what happens when Dodo moves to a different street? Do we have to construct a new object from scratch?
[[amending-objects]]
=== Amending Objects
Fortunately, we don't have to.
An object can be _amended_ to form a new object that only differs in selected properties.
Here is how this looks:
[source%parsed,{pkl}]
----
tortoise = (dodo) { // <1>
name = "Galápagos tortoise"
taxonomy { // <2>
`class` = "Reptilia" // <3>
}
}
----
<1> Defines a module property named `tortoise`.
Its value is an object that _amends_ `dodo`.
Note that the amended object must be enclosed in parentheses.
<2> Object property `tortoise.taxonomy` _amends_ `dodo.taxonomy`.
<3> Object property `tortoise.taxonomy.class` _overrides_ `dodo.taxonomy.class`.
As you can see, it is easy to construct a new object that overrides selected properties of an existing object,
even if, as in our example, the overridden property is nested inside another object.
NOTE: If this way of constructing new objects from existing objects reminds you of prototypical inheritance, you are spot-on:
Pkl objects use prototypical inheritance as known from languages such as JavaScript.
But unlike in JavaScript, their prototype chain cannot be directly accessed or even modified.
Another difference is that in Pkl, object properties are late-bound. Read on to see what this means.
[[amends-declaration]]
[NOTE]
.Amends declaration vs. amends expression
====
The <<defining-objects,defining objects>> and <<amending-objects,amending objects>> sections cover two notations that are both a form of amending; called an _amends declaration_ and an _amends expression_, respectively.
[source%tested,{pkl}]
----
pigeon { // <1>
name = "Turtle dove"
extinct = false
}
parrot = (pigeon) { // <2>
name = "Parrot"
}
----
<1> Amends declaration.
<2> Amends expression.
An amends declaration amends a property of the same name if the property exists within a parent module.
Otherwise, an amends declaration implicitly amends {uri-stdlib-Dynamic}[Dynamic].
Another way to think about an amends declaration is that it is shorthand for assignment.
In practical terms, `pigeon {}` is the same as `pigeon = (super.pigeon) {}`.
Amending object bodies can be chained for both an amends declaration and an amends expression.
[source%tested,{pkl}]
----
pigeon {
name = "Common wood pigeon"
} {
extinct = false
} // <1>
dodo = (pigeon) {
name = "Dodo"
} {
extinct = true
} // <2>
----
<1> Chained amends declaration.
<2> Chained amends expression (`(pigeon) { ... } { ... }` is the amends expression).
====
[[late-binding]]
=== Late Binding
Let's move on to Pkl's secret sauce:
the ability to define an object property's value in terms of another property's value, and the resulting _late binding_ effect.
Here is an example:
[source%tested,{pkl}]
----
penguin {
eggIncubation = 40.d
adultWeightInGrams = eggIncubation.value * 100 // <1>
}
adultWeightInGrams = penguin.adultWeightInGrams
----
<1> result: `4000`
We have defined a hypothetical `penguin` object whose `adultWeightInGrams` property is defined in terms of the `eggIncubation` duration.
Can you guess what happens when `penguin` is amended and its `eggIncubation` overridden?
[source%tested,{pkl}]
----
madeUpBird = (penguin) {
eggIncubation = 11.d
}
adultWeightInGrams = madeUpBird.adultWeightInGrams // <1>
----
<1> result: `1100`
As you can see, ``madeUpBird``'s `adultWeightInGrams` changed along with its `eggIncubation`.
This is what we mean when we say that object properties are _late-bound_.
[NOTE]
.Spreadsheet Programming
====
A good analogy is that object properties behave like spreadsheet cells.
When they are linked, changes to "downstream" properties automatically propagate to "upstream" properties.
The main difference is that editing a spreadsheet cell changes the state of the spreadsheet,
whereas "editing" a property results in a new object, leaving the original object untouched.
It is as if you made a copy of the entire spreadsheet whenever you edited a cell!
====
Late binding of properties is an incredibly useful feature for a configuration language.
It is used extensively in Pkl code (especially in templates) and is the key to understanding how Pkl works.
=== Transforming Objects
Say we have the following object:
[source%tested,{pkl}]
----
dodo {
name = "Dodo"
extinct = true
}
----
How can property `name` be removed?
The recipe for transforming an object is:
. Convert the object to a map.
. Transform the map using ``Map``'s link:{uri-stdlib-Map}[rich API].
. If necessary, convert the map back to an object.
Equipped with this knowledge, let's try to accomplish our objective:
[source%tested,{pkl-expr}]
----
dodo
.toMap()
.remove("name")
.toDynamic()
----
The resulting dynamic object is equivalent to `dodo`, except that it no longer has a `name` property.
[IMPORTANT]
.Lazy vs. Eager Data Types
====
Converting an object to a map is a transition from a _lazy_ to an _eager_ data type.
All of the object's properties are evaluated and all references between them are resolved.
If the map is later converted back to an object, subsequent changes to the object's properties no longer propagate to (previously) dependent properties.
To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toMap()` or `toDynamic()`.
====
[[typed-objects]]
=== Typed Objects
:fn-typed-objects: footnote:[By "structure" we mean a list of property names and (optionally) property types.]
Pkl has two kinds of objects:
* A link:{uri-stdlib-Dynamic}[Dynamic] object has no predefined structure.{fn-typed-objects}
When a dynamic object is amended, not only can existing properties be overridden or amended, but new properties can also be added.
So far, we have only used dynamic objects in this chapter.
* A link:{uri-stdlib-Typed}[Typed] object has a fixed structure described by a class definition.
When a typed object is amended, its properties can be overridden or amended, but new properties cannot be added.
In other words, the new object has the same class as the original object.
[TIP]
.When to Use Typed vs. Dynamic Objects
====
* Use typed objects to build schema-backed data models that are validatedfootnote:[By "Use typed objects" we mean to define classes and build data models out of instances of these classes.].
This is what most templates do.
* Use dynamic objects to build schema-less data models that are not validated.
Dynamic objects are useful for ad-hoc tasks, tasks that do not justify the effort of writing and maintaining a schema, and for representing data whose structure is unknown.
====
Note that every <<modules,module>> is a typed object. Its properties implicitly define a class,
and new properties cannot be added when amending the module.
A typed object is backed by a _class_.
Let's look at an example:
[source%tested,{pkl}]
----
class Bird { // <1>
name: String
lifespan: Int
migratory: Boolean
}
pigeon = new Bird { // <2>
name = "Pigeon"
lifespan = 8
migratory = false
}
----
<1> Defines a class named `Bird` with properties `name`, `lifespan` and `migratory`.
<2> Defines a module property named `pigeon`.
Its value is a typed object constructed by instantiating class `Bird`.
A type only needs to be stated when the property does not have or inherit a <<type-annotations,type annotation>>.
Otherwise, amend syntax (`pigeon { ... }`) or shorthand instantiation syntax (`pigeon = new { ... }`) should be used.
Congratulations, you have constructed your first typed objectfootnote:[Not counting that every module is a typed object.]!
How does it differ from a dynamic object?
The answer is that a typed object has a fixed structure prescribed by its class, which cannot be changed when amending the object:
[source%tested%error,{pkl}]
----
class Bird { // <1>
name: String
lifespan: Int
}
faultyPigeon = new Bird {
name = "Pigeon"
lifespan = 8
hobby = "singing"
}
----
Evaluating this gives:
[source,shell,subs="quotes"]
----
Cannot find property *hobby* in object of type *repl#Bird*.
Available properties:
lifespan
name
----
Class structure is also enforced when instantiating a class.
Let's try to override property `name` with a value of the wrong type:
[source%tested%error,{pkl}]
----
faultyPigeon2 = new Bird {
name = 3.min
lifespan = 8
}
----
Evaluating this, also fails:
[source,shell,subs="quotes"]
----
Expected value of type *String*, but got type *Duration*.
Value: 3.min
----
Typed objects are the fundamental building block for constructing validated data models in Pkl.
To dive deeper into this topic, continue with <<classes,Classes>>.
[NOTE]
.Converting untyped objects to typed objects
====
When you have a `Dynamic` that has all the properties (with the right types and meeting all constraints), you can convert it to a `Typed` by using link:{uri-stdlib-Dynamic-toTyped}[`toTyped(<class>)`]:
[source,{pkl}]
----
class Bird {
name: String
lifespan: Int
}
pigeon = new Dynamic { // <1>
name = "Pigeon"
lifespan = 8
}.toTyped(Bird) // <2>
----
<1> Instead of a `new Bird`, `pigeon` can be defined with a `new Dynamic`.
<2> That `Dynamic` is then converted to a `Bird`.
====
[[properties]]
=== Property Modifiers
==== Hidden Properties
A property with the modifier `hidden` is omitted from the rendered output and object conversions.
Hidden properties are also ignored when evaluating equality or hashing (e.g. for `Mapping` or `Map` keys).
[source,{pkl}]
----
class Bird {
name: String
lifespan: Int
hidden nameAndLifespanInIndex = "\(name), \(lifespan)" // <1>
nameSignWidth: UInt = nameAndLifespanInIndex.length // <2>
}
pigeon = new Bird { // <3>
name = "Pigeon"
lifespan = 8
}
pigeonInIndex = pigeon.nameAndLifespanInIndex // <4>
pigeonDynamic = pigeon.toDynamic() // <5>
favoritePigeon = (pigeon) {
nameAndLifespanInIndex = "Bettie, \(lifespan)"
}
samePigeon = pigeon == favoritePigeon // <6>
----
<1> Properties defined as `hidden` are accessible on any `Bird` instance, but not output by default.
<2> Non-hidden properties can refer to hidden properties as usual.
<3> `pigeon` is an object with _four_ properties, but is rendered with _three_ properties.
<4> Accessing a `hidden` property from outside the class and object is like any other property.
<5> Object conversions omit hidden properties, so the resulting `Dynamic` has three properties.
<6> Objects that differ only in `hidden` property values are considered equal
Invoking Pkl on this file produces the following result.
[source,{pkl}]
----
pigeon {
name = "Pigeon"
lifespan = 8
nameSignWidth = 9
}
pigeonInIndex = "Pigeon, 8"
pigeonDynamic {
name = "Pigeon"
lifespan = 8
nameSignWidth = 9
}
favoritePigeon {
name = "Pigeon"
lifespan = 8
nameSignWidth = 9
}
samePigeon = true
----
==== Local properties
A property with the modifier `local` can only be referenced in the lexical scope of its definition.
[source,{pkl}]
----
class Bird {
name: String
lifespan: Int
local separator = "," // <1>
hidden nameAndLifespanInIndex = "\(name)\(separator) \(lifespan)" // <2>
}
pigeon = new Bird {
name = "Pigeon"
lifespan = 8
}
pigeonInIndex = pigeon.nameAndLifespanInIndex // <3>
pigeonSeparator = pigeon.separator // Error <4>
----
<1> This property can only be accessed from inside this _class definition_.
<2> Non-local properties can refer to the local property as usual.
<3> The _value_ of `separator` occurs in `nameAndLifespanInIndex`.
<4> Pkl does not accept this, as there is no property `separator` on a `Bird` instance.
Because a `local` property is added to the lexical scope, but not (observably) to the object, you can add `local` properties to ``Listing``s and ``Mapping``s.
[NOTE]
.Import clauses define local properties
====
An _import clause_ defines a local property in the containing module.
This means `import "someModule.pkl"` is effectively `const local someModule = import("someModule.pkl")`.
Also, `import "someModule.pkl" as otherName` is effectively `const local otherName = import("someModule.pkl")`.
====
[[fixed-properties]]
==== Fixed properties
A property with the `fixed` modifier cannot be assigned to or amended when defining an object of its class.
.Bird.pkl
[source%tested,{pkl}]
----
fixed laysEggs: Boolean = true
fixed birds: Listing<String> = new {
"Pigeon"
"Hawk"
"Penguin"
}
----
When amending, assigning to a `fixed` property is an error.
Similarly, it is an error to use an <<amends-declaration,amends declaration>> on a fixed property:
.invalid.pkl
[source%tested,{pkl}]
----
amends "Bird.pkl"
laysEggs = false // <1>
birds { // <2>
"Giraffe"
}
----
<1> Error: cannot assign to fixed property `laysEggs`
<2> Error: cannot amend fixed property `birds`
When extending a class and overriding an existing property definition, the fixedness of the overridden property must be preserved.
If the property in the parent class is declared `fixed`, the child property must also be declared `fixed`.
If the property in the parent class is not declared `fixed`, the child property may not add the `fixed` modifier.
[source%parsed,{pkl}]
----
abstract class Bird {
fixed canFly: Boolean
name: String
}
class Penguin extends Bird {
canFly = false // <1>
fixed name = "Penguin" // <2>
}
----
<1> Error: missing modifier `fixed`.
<2> Error: modifier `fixed` cannot be applied to property `name`.
The `fixed` modifier is useful for defining properties that are meant to be derived from other properties.
In the following snippet, the property `wingspanWeightRatio` is not meant to be assigned to, because it is derived
from other properties.
[source%parsed,{pkl}]
----
class Bird {
wingspan: Int
weight: Int
fixed wingspanWeightRatio: Int = wingspan / weight
}
----
Another use-case for `fixed` is to define properties that are meant to be fixed to a class definition.
In the example below, the `species` of a bird is tied to the class, and therefore is declared `fixed`.
Note that it is possible to define a `fixed` property without a value, for one of two reasons:
1. The type has a default value that makes an explicit default redundant.
2. The property is meant to be overridden by a child class.
[source%tested,{pkl}]
----
abstract class Bird {
fixed species: String // <1>
}
class Osprey extends Bird {
fixed species: "Pandion haliaetus" // <2>
}
----
<1> No explicit default because the property is overridden by a child class.
<2> Overrides the type from `String` to the <<string-literal-types,string literal type>> `"Pandion haliaetus"`. +
Assigning an explicit default would be redundant, therefore it is omitted.
==== Const properties
A property with the `const` modifier behaves like the <<fixed-properties,`fixed`>> modifier,
with the additional rule that it cannot reference non-const properties or methods.
.Bird.pkl
[source%tested,{pkl}]
----
const laysEggs: Boolean = true
const examples: Listing<String> = new {
"Pigeon"
"Hawk"
"Penguin"
}
----
Referencing any non-const property or method is an error.
.invalid.pkl
[source%parsed,{pkl}]
----
pigeonName: String = "Pigeon"
const function birdLifespan(i: Int): Int = (i / 4).toInt()
class Bird {
name: String
lifespan: Int
}
const bird: Bird = new {
name = pigeonName // <1>
lifespan = birdLifespan(24) // <2>
}
----
<1> Error: cannot reference non-const property `pigeonName` from a const property.
<2> Allowed: `birdLifespan` is const.
It is okay to reference another value _within_ the same const property.
.valid.pkl
[source%tested,{pkl}]
----
class Bird {
lifespan: Int
description: String
speciesName: "Bird"
}
const bird: Bird = new {
lifespan = 8
description = "species: \(speciesName), lifespan: \(lifespan)" // <1>
}
----
<1> `lifespan` is declared within property `bird`. `speciesName` resolves to `this.speciesName`, where `this` is a value within property `bird`.
NOTE: Because `const` members can only reference themselves and other `const` members, they are not <<late-binding,late bound>>.
The `const` modifier implies that it is also <<fixed-properties,fixed>>.
Therefore, the same rules that apply to `fixed` also apply to `const`:
* A `const` property cannot be assigned to or amended when defining an object of its class.
* The const-ness of a property or method must be preserved when it is overridden by a child class.
[[class-and-annotation-const]]
*Class, Annotation, and Typealias Scoping*
In these following scenarios, any reference to a property or method of its enclosing module requires that the referenced member is `const`:
* Class body
* Annotation body
* Typealiased constrained type
.invalid2.pkl
[source%parsed,{pkl}]
----
pigeonName: String = "Pigeon"
class Bird {
name: String = pigeonName // <1>
}
@Deprecated { message = "Replace with \(pigeonName)" } // <2>
oldPigeonName: String
typealias IsPigeonName = String(pigeonName) // <3>
----
<1> Error: cannot reference non-const property `pigeonName` from a class.
<2> Error: cannot reference non-const property `pigeonName` from an annotation.
<3> Error: cannot reference non-const property `pigeonname` from a typealias.
This rule exists because classes, annotations, and typealiases are not <<late-binding,late bound>>;
it is not possible to change the definition of these members by amending the module
where it is defined.
Generally, there are two strategies for referencing such properties:
*Add the `const` modifier to the referenced property*
One solution is to add the `const` modifier to the property being referenced.
.Birds.pkl
[source,diff]
----
-pigeonName: String = "Pigeon"
+const pigeonName: String = "Pigeon"
class Bird {
name: String = pigeonName
}
----
This solution makes sense if `pigeonName` does not get assigned/amended when amending module `Birds.pkl` (modules are regular objects that can be amended).
*Self-import the module*
.Birds.pkl
[source,diff]
----
+import "Birds.pkl" // <1>
+
pigeonName: String = "Pigeon"
class Bird {
- name: String = pigeonName
+ name: String = Birds.pigeonName
}
----
<1> module `Birds` imports itself
This solution works because an import clause implicitly defines a `const local` property and amending this module does not affect a self-import.
This makes sense if property `pigeonName` *does* get assigned/amended when amending module `Birds.pkl`.
[[listings]]
== Listings
A value of type link:{uri-stdlib-Listing}[Listing] is an ordered, indexed collection of _elements_.
A listing's elements have zero-based indexes and are lazily evaluated on the first read.
Listings combine qualities of lists and objects:
* Like lists, listings can contain arbitrary elements.
* Like objects, listings excel at defining and amending nested literal data structures.
* Like objects, listings can only be directly manipulated through amendment,
but converting them to a list (and, if necessary, back to a listing) opens the door to arbitrary transformations.
* Like object properties, listing elements are evaluated lazily, can be defined in terms of each other, and are late-bound.
[TIP]
.When to use Listing vs. <<lists,List>>
====
* When a collection of elements needs to be specified literally, use a listing.
* When a collection of elements needs to be transformed in a way that cannot be achieved by <<amending-listings,amending>> a listing, use a list.
* If in doubt, use a listing.
Templates and schemas should almost always use listings instead of lists.
Note that listings can be converted to lists when the need arises.
====
=== Defining Listings
Listings have a literal syntax that is similar to that of objects.
Here is a listing with two elements:
[source%tested,{pkl}]
----
birds = new Listing { // <1>
new { // <2>
name = "Pigeon"
diet = "Seed"
}
new { // <3>
name = "Parrot"
diet = "Berries"
}
}
----
<1> Defines a module property named `birds` with a value of type `Listing`.
A type only needs to be stated when the property does not have or inherit a <<listing-type-annotations,type annotation>>.
Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used.
<2> Defines a listing element of type `Dynamic`.
<3> Defines another listing element of type `Dynamic`.
The order of definitions is relevant.
To access an element by index, use the `[]` (subscript) operator:
[source%tested,{pkl}]
----
firstBirdName = birds[0].name // <1>
secondBirdDiet = birds[1].diet // <2>
----
<1> result: `"Pigeon"`
<2> result: `"Berries"`
Listings can contain arbitrary types of elements:
[source%tested,{pkl}]
----
listing = new Listing {
"Pigeon" // <1>
3.min // <2>
new Listing { // <3>
"Barn owl"
}
}
----
<1> Defines a listing element of type `String`.
<2> Defines a listing element of type `Duration`.
<3> Defines a listing element of type `Listing`.
Listings can have `local` properties:
[source%tested,{pkl}]
----
listing = new Listing {
local pigeon = "Pigeon" // <1>
pigeon // <2>
"A " + pigeon + " is a bird" // <3>
}
----
<1> Defines a local property with the value `"Pigeon"`.
Local properties can have a type annotation, as in `pigeon: String = "Pigeon"`.
<2> Defines a listing element that references the local property.
<3> Defines another listing element that references the local property.
[[amending-listings]]
=== Amending Listings
Let's say we have the following listing:
[source%tested,{pkl}]
----
birds = new Listing {
new {
name = "Pigeon"
diet = "Seeds"
}
new {
name = "Parrot"
diet = "Berries"
}
}
----
To add, override, or amend elements of this listing, amend the listing itself:
[source%tested,{pkl}]
----
birds2 = (birds) { // <1>
new { // <2>
name = "Barn owl"
diet = "Mice"
}
[0] { // <3>
diet = "Worms"
}
[1] = new { // <4>
name = "Albatross"
diet = "Fish"
}
}
----
<1> Defines a module property named `birds2`. Its value is a listing that amends `birds`.
<2> Defines a listing element of type `Dynamic`.
<3> Amends the listing element at index 0 (whose name is `"Pigeon"`) and overrides property `diet`.
<4> Overrides the listing element at index 1 (whose name is `"Parrot"`) with an entirely new dynamic object.
=== Late Binding
A listing element can be defined in terms of another element.
To reference the element at index `<index>`, use `this[<index>]`:
[source%tested,{pkl}]
----
birds = new Listing {
new { // <1>
name = "Pigeon"
diet = "Seeds"
}
(this[0]) { // <2>
name = "Parrot"
}
}
----
<1> Defines a listing element of type `Dynamic`.
<2> Defines a listing element that amends the element at index 0 and overrides `name`.
Listing elements are late-bound:
[source%tested,{pkl}]
----
newBirds = (birds) { // <1>
[0] {
diet = "Worms"
}
}
secondBirdDiet = newBirds[1].diet // <2>
----
<1> Amends listing `birds` and overrides property `diet` of element 0 (whose name is "Pigeon"`) to have the value `"Worms"`.
<2> Because element 1 is defined in terms of element 0, its `diet` property also changes to `"Worms"`.
=== Transforming Listings
Say we have the following listing:
[source%tested,{pkl}]
----
birds = new Listing {
new {
name = "Pigeon"
diet = "Seeds"
}
new {
name = "Parrot"
diet = "Berries"
}
}
----
How can the order of elements be reversed programmatically?
The recipe for transforming a listing is:
. Convert the listing to a list.
. Transform the list using ``List``'s link:{uri-stdlib-List}[rich API].
. If necessary, convert the list back to a listing.
TIP: Often, transformations happen in a link:{uri-stdlib-PcfRenderer-converters}[converter] of a link:{uri-stdlib-BaseValueRenderer}[value renderer].
Because most value renderers treat lists the same as listings, it is often not necessary to convert back to a listing.
Equipped with this knowledge, let's try to accomplish our objective:
[source%tested,{pkl}]
----
reversedbirds = birds
.toList()
.reverse()
.toListing()
----
`result` now contains the same elements as `birds`, but in reverse order.
[IMPORTANT]
.Lazy vs. Eager Data Types
====
Converting a listing to a list is a transition from a _lazy_ to an _eager_ data type.
All of the listing's elements are evaluated and all references between them are resolved.
If the list is later converted back to a listing, subsequent changes to the listing's elements no longer propagate to (previously) dependent elements.
To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toList()` or `toListing()`.
====
=== Default Element
Listings can have a _default element_:
[source%tested,{pkl}]
----
birds = new Listing {
default { // <1>
lifespan = 8
}
new { // <2>
name = "Pigeon" // <3>
}
new { // <4>
name = "Parrot"
lifespan = 20 // <5>
}
}
----
<1> Amends the `default` element and sets property `lifespan`.
<2> Defines a new listing element that implicitly amends the default element.
<3> Defines a new property called `name`. Property `lifespan` is inherited from the default element.
<4> Defines a new listing element that implicitly amends the default element.
<5> Overrides the default for property `lifespan`.
`default` is a hidden (that is, not rendered) link:{uri-stdlib-Listing-default}[property] defined in class `Listing`.
If `birds` had a <<listing-type-annotations,type annotation>>, a suitable default element would be inferred from its type parameter.
If, as in our example, no type annotation is provided or inherited, the default element is the empty `Dynamic` object.
Like regular listing elements, the default element is late-bound.
As a result, defaults can be changed retroactively:
[source%tested,{pkl}]
----
birds2 = (birds) {
default {
lifespan = 8
diet = "Seeds"
}
}
----
Because both of ``birds``'s elements amend the default element, changing the default element also changes them.
An equivalent literal definition of `birds2` would look as follows:
[source%tested,{pkl}]
----
birds2 = new Listing {
new {
name = "Pigeon"
lifespan = 8
diet = "Seeds"
}
new {
name = "Parrot"
lifespan = 20
diet = "Berries"
}
}
----
Note that Parrot kept its diet because its prior self defined it explicitly, overriding any default.
If you are interested in the technical underpinnings of default elements (and not afraid of dragons!), continue with <<function-amending, Function Amending>>.
[[listing-type-annotations]]
=== Type Annotations
To declare the type of a property that is intended to hold a listing, use:
[source,{pkl}]
----
x: Listing<ElementType>
----
This declaration has the following effects:
* `x` is initialized with an empty listing.
* If `ElementType` has a <<default-values,default value>>, that value becomes the listing's default element.
* The first time `x` is read,
** its value is checked to have type `Listing`.
** the listing's elements are checked to have the type `ElementType`.
Here is an example:
[source%tested,{pkl}]
----
class Bird {
name: String
lifespan: Int
}
birds: Listing<Bird>
----
Because the default value for type `Bird` is `new Bird {}`, that value becomes the listing's default element.
Let's go ahead and populate `birds`:
[source%tested,{pkl}]
----
birds {
new {
name = "Pigeon"
lifespan = 8
}
new {
name = "Parrot"
lifespan = 20
}
}
----
Thanks to ``birds``'s default element, which was inferred from its type,
it is not necessary to state the type of each list element
(`new Bird { ... }`, `new Bird { ... }`, etc.).
==== Distinct Elements
To constrain a listing to distinct elements, use ``Listing``'s link:{uri-stdlib-Listing-isDistinct}[isDistinct] property:
[source%tested,{pkl}]
----
class Bird {
name: String
lifespan: Int
}
birds: Listing<Bird>(isDistinct)
----
This is as close as Pkl's late-bound data types (objects, listings, and mappings) get to a <<sets,Set>>.
To demand distinct names instead of distinct `Bird` objects, use link:{uri-stdlib-Listing-isDistinctBy}[isDistinctBy()]:
[source%tested,{pkl}]
----
birds: Listing<Bird>(isDistinctBy((it) -> it.name))
----
[[mappings]]
== Mappings
A value of type link:{uri-stdlib-Mapping}[Mapping] is an ordered collection of _values_ indexed by _key_.
NOTE: Most of what has been said about <<listings,listings>> also applies to mappings.
Nevertheless, this section is written to stand on its own.
A mapping's key-value pairs are called its _entries_.
Keys are eagerly evaluated; values are lazily evaluated on the first read.
Mappings combine qualities of maps and objects:
* Like maps, mappings can contain arbitrary key-value pairs.
* Like objects, mappings excel at defining and amending nested literal data structures.
* Like objects, mappings can only be directly manipulated through amendment,
but converting them to a map (and, if necessary, back to a mapping) opens the door to arbitrary transformations.
* Like object properties, a mapping's values (but not its keys) are evaluated lazily, can be defined in terms of each other, and are late-bound.
[TIP]
.When to use Mapping vs. <<maps,Map>>
====
* When key-value style data needs to be specified literally, use a mapping.
* When key-value style data needs to be transformed in a way that cannot be achieved by <<amending-mappings,amending>> a mapping, use a map.
* If in doubt, use a mapping.
Templates and schemas should almost always use mappings instead of maps.
Note that mappings can be converted to maps when the need arises.
====
=== Defining Mappings
Mappings have the same literal syntax as objects, except that keys enclosed in `[]` take the place of property names.
Here is a mapping with two entries:
[source%tested,{pkl}]
----
birds = new Mapping { // <1>
["Pigeon"] { // <2>
lifespan = 8
diet = "Seeds"
}
["Parrot"] { // <3>
lifespan = 20
diet = "Berries"
}
}
----
<1> Defines a module property named `birds` with a value of type `Mapping`.
A type only needs to be stated when the property does not have or inherit a <<mapping-type-annotations,type annotation>>.
Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used.
<2> Defines a mapping entry with the key `"Pigeon"` and a value of type `Dynamic`.
<3> Defines a mapping entry with the key `"Parrot"` and a value of type `Dynamic`.
To access a value by key, use the `[]` (subscript) operator:
[source%tested,{pkl}]
----
pigeon = birds["Pigeon"]
parrot = birds["Parrot"]
----
Mappings can contain arbitrary types of values:
[source%tested,{pkl}]
----
mapping = new Mapping {
["number"] = 42
["list"] = List("Pigeon", "Parrot")
["nested mapping"] {
["Pigeon"] {
lifespan = 20
diet = "Seeds"
}
}
}
----
Although string keys are most common, mappings can contain arbitrary types of keys:
[source%tested,{pkl}]
----
mapping = new Mapping {
[3.min] = 42
[new Dynamic { name = "Pigeon" }] = "abc"
}
----
Keys can be computed:
[source%tested,{pkl}]
----
mapping = new Mapping {
["Pigeon".reverse()] = 42
}
----
Mappings can have `local` properties:
[source%tested,{pkl}]
----
mapping = new Mapping {
local parrot = "Parrot" // <1>
["Pigeon"] { // <2>
friend = parrot
}
}
----
<1> Defines a local property name `parrot` with the value `"Parrot"`.
Local properties can have a type annotation, as in `parrot: String = "Parrot"`.
<2> Defines a mapping entry whose value references `parrot`.
The local property is visible to values but not keys.
[[amending-mappings]]
=== Amending Mappings
Let's say we have the following mapping:
[source%tested,{pkl}]
----
birds = new Mapping {
["Pigeon"] {
lifespan = 8
diet = "Seeds"
}
["Parrot"] {
lifespan = 20
diet = "Berries"
}
}
----
To add, override, or amend entries of this mapping, amend the mapping:
[source%tested,{pkl}]
----
birds2 = (birds) { // <1>
["Barn owl"] { // <2>
lifespan = 15
diet = "Mice"
}
["Pigeon"] { // <3>
diet = "Seeds"
}
["Parrot"] = new { // <4>
lifespan = 20
diet = "Berries"
}
}
----
<1> Defines a module property named `birds2`. Its value is a mapping that amends `birds`.
<2> Defines a mapping entry with the key `"Barn owl"` and a value of type `Dynamic`.
<3> Amends mapping entry `"Pigeon"` and overrides property `diet`.
<4> Overrides mapping entry `"Parrot"` with an entirely new value of type `Dynamic`.
=== Late Binding
A mapping entry's value can be defined in terms of another entry's value.
To reference the value with key `<key>`, use `this[<key>]`:
[source%tested,{pkl}]
----
birds = new Mapping {
["Pigeon"] { // <1>
lifespan = 8
diet = "Seeds"
}
["Parrot"] = (this["Pigeon"]) { // <2>
lifespan = 20
}
}
----
<1> Defines a mapping entry with the key `"Pigeon"` and a value of type `Dynamic`.
<2> Defines a mapping entry with the key `"Parrot"` and a value that amends `"Pigeon"`.
Mapping values are late-bound:
[source%tested,{pkl}]
----
birds2 = (birds) { // <1>
["Pigeon"] {
diet = "Seeds"
}
}
parrotDiet = birds2["Parrot"].diet // <2>
----
<1> Amends mapping `birds` and overrides ``"Pigeon"``'s `diet` property to have value `"Seeds"`.
<2> Because `"Parrot"` is defined in terms of `"Pigeon"`, its `diet` property also changes to `"Seeds"`.
=== Transforming Mappings
Say we have the following mapping:
[source%tested,{pkl}]
----
birds = new Mapping {
["Pigeon"] {
lifespan = 8
diet = "Seeds"
}
["Parrot"] = (this["Pigeon"]) {
lifespan = 20
}
}
----
How can ``birds``'s keys be reversed programmatically?
The recipe for transforming a mapping is:
. Convert the mapping to a map.
. Transform the map using ``Map``'s link:{uri-stdlib-Map}[rich API].
. If necessary, convert the map back to a mapping.
TIP: Often, transformations happen in a link:{uri-stdlib-PcfRenderer-converters}[converter] of a link:{uri-stdlib-BaseValueRenderer}[value renderer].
As most value renderers treat maps the same as mappings, it is often not necessary to convert back to a mapping.
Equipped with this knowledge, let's try to accomplish our objective:
[source%tested,{pkl}]
----
result = birds
.toMap()
.mapKeys((key, value) -> key.reverse())
.toMapping()
----
`result` contains the same values as `birds`, but its keys have changed to `"noegiP"` and `"torraP"`.
[IMPORTANT]
.Lazy vs. Eager Data Types
====
Converting a mapping to a map is a transition from a _lazy_ to an _eager_ data type.
All of the mapping's values are evaluated and all references between them are resolved.
(Mapping keys are eagerly evaluated.)
If the map is later converted back to a mapping, changes to the mapping's values no longer propagate to (previously) dependent values.
To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toMap()` or `toMapping()`.
====
=== Default Value
Mappings can have a _default value_:
[source%tested,{pkl}]
----
birds = new Mapping {
default { // <1>
lifespan = 8
}
["Pigeon"] { // <2>
diet = "Seeds" // <3>
}
["Parrot"] { // <4>
lifespan = 20 // <5>
}
}
----
<1> Amends the `default` value and sets property `lifespan`.
<2> Defines a mapping entry with the key `"Pigeon"` that implicitly amends the default value.
<3> Defines a new property called `diet`. Property `lifespan` is inherited from the default value.
<4> Defines a mapping entry with the key `"Parrot"` that implicitly amends the default value.
<5> Overrides the default for property `lifespan`.
`default` is a hidden (that is, not rendered) link:{uri-stdlib-Mapping-default}[property] defined in class `Mapping`.
If `birds` had a <<mapping-type-annotations,type annotation>>, a suitable default value would be inferred from its second type parameter.
If, as in our example, no type annotation is provided or inherited, the default value is the empty `Dynamic` object.
Like regular mapping values, the default value is late-bound.
As a result, defaults can be changed retroactively:
[source%tested,{pkl}]
----
birds2 = (birds) {
default {
lifespan = 8
diet = "Seeds"
}
}
----
Because both of ``birds``'s mapping values amend the default value, changing the default value also changes them.
An equivalent literal definition of `birds2` would look as follows:
[source%tested,{pkl}]
----
birds2 = new Mapping {
["Pigeon"] {
lifespan = 8
diet = "Seeds"
}
["Parrot"] {
lifespan = 20
diet = "Berries"
}
}
----
Note that Parrot kept its lifespan because its prior self defined it explicitly, overriding any default.
If you are interested in the technical underpinnings of default values, continue with <<function-amending, Function Amending>>.
[[mapping-type-annotations]]
=== Type Annotations
To declare the type of a property that is intended to hold a mapping, use:
[source,{pkl}]
----
x: Mapping<KeyType, ValueType>
----
This declaration has the following effects:
* `x` is initialized with an empty mapping.
* If `ValueType` has a <<default-values,default value>>, that value becomes the mapping's default value.
* The first time `x` is read,
** its value is checked to have type `Mapping`.
** the mapping's keys are checked to have type `KeyType`.
** the mapping's values are checked to have type `ValueType`.
Here is an example:
[source%tested,{pkl}]
----
class Bird {
lifespan: Int
}
birds: Mapping<String, Bird>
----
Because the default value for type `Bird` is `new Bird {}`, that value becomes the mapping's default value.
Let's go ahead and populate `birds`:
[source%tested,{pkl}]
----
birds {
["Pigeon"] {
lifespan = 8
}
["Parrot"] {
lifespan = 20
}
}
----
Thanks to ``birds``'s default value, which was inferred from its type,
it is not necessary to state the type of each mapping value
(`["Pigeon"] = new Bird { ... }`, `["Parrot"] = new Bird { ... }`, etc.).
[[classes]]
== Classes
Classes are arranged in a single inheritance hierarchy.
At the top of the hierarchy sits class link:{uri-stdlib-Any}[Any]; at the bottom, type <<nothing-type,nothing>>.
Classes contain properties and methods, which can be `local` to their declaring scope.
Properties can also be `hidden` from rendering.
[source%tested,{pkl}]
----
class Bird {
name: String
hidden taxonomy: Taxonomy
}
class Taxonomy {
`species`: String
}
pigeon: Bird = new {
name = "Common wood pigeon"
taxonomy {
species = "Columba palumbus"
}
}
pigeonClass = pigeon.getClass()
----
Declaration of new class instances will fail when property names are misspelled:
[source%tested%error,{pkl}]
----
// Detects the spelling mistake
parrot = new Bird {
namw = "Parrot"
}
----
=== Class Inheritance
Pkl supports single inheritance with a Java(Script)-like syntax.
[source%tested,{pkl}]
----
abstract class Bird {
name: String
}
class ParentBird extends Bird {
kids: List<String>
}
pigeon: ParentBird = new {
name = "Old Pigeon"
kids = List("Pigeon Jr.", "Teen Pigeon")
}
----
[[methods]]
== Methods
Pkl methods can be defined on classes and modules using the `function` keyword.
Methods may access properties of their containing type.
Submodules and subclasses can override them.
Like Java and most other object-oriented languages, Pkl uses _single dispatch_ -- methods are dynamically dispatched based on the receiver's runtime type.
[source%tested,{pkl}]
----
class Bird {
name: String
function greet(bird: Bird): String = "Hello, \(bird.name)!" // <1>
}
function greetPigeon(bird: Bird): String = bird.greet(pigeon) // <2>
pigeon: Bird = new {
name = "Pigeon"
}
parrot: Bird = new {
name = "Parrot"
}
greeting1 = pigeon.greet(parrot) // <3>
greeting2 = greetPigeon(parrot) // <4>
----
<1> Instance method of class `Bird`.
<2> Module method.
<3> Call instance method on `pigeon`.
<4> Call module method (on `this`).
Like other object-oriented languages, methods defined on extended classes and modules may be overridden.
The parent type's method may be called via the <<super-keyword,`super` keyword>>.
NOTE: Methods do not support named parameters or default parameter values.
The xref:blog:ROOT:class-as-a-function.adoc[Class-as-a-function] pattern may be a suitable replacement.
TIP: In most cases, methods without parameters should not be defined.
Instead, use <<fixed-properties,`fixed` properties>> on the module or class.
[[modules]]
== Modules
=== Introduction
Modules are the unit of loading, executing, and sharing Pkl code.
Every file containing Pkl code is a module.
By convention, module files have a `.pkl` extension.
Modules have a <<Module Names,Module Name>> and are loaded from a <<Module URIs,Module URI>>.
At runtime, modules are represented as objects of type link:{uri-stdlib-baseModule}/Module[Module].
The precise runtime type of a module is a subclass of `Module` containing the module's property and method definitions.
Like class members, module members may have type annotations, which are validated at runtime:
[source%tested,{pkl}]
----
timeout: Duration(isPositive) = 5.ms
function greet(name: String): String = "Hello, \(name)!"
----
Because modules are regular objects, they can be assigned to properties and passed to and returned from methods.
Modules can be <<import-module,imported>> by other modules.
In analogy to objects, modules can serve as templates for other modules through <<module-amend,amending>>.
In analogy to classes, modules can be <<module-extend,extended>> to add additional module members.
=== Module Names
Modules may declare their name by way of a _module clause_, which consists of the keyword `module` followed by a qualified name:
[source%tested,{pkl}]
----
/// My bird module.
module com.animals.Birds
----
A module clause must come first in a module.
Its doc comment, if present, holds the module's overall documentation.
In the absence of a module clause, a module's name is inferred from the module URI from which the module was first loaded.
For example, the inferred name for a module first loaded from `+https://example.com/pkl/bird.pkl+` is `bird`.
Module names do not affect evaluation but are used in diagnostic messages and Pkldoc.
In particular, they are the first component (everything before the hash sign) of fully qualified member names such as `pkl.base#Int`.
NOTE: Modules shared with other parties should declare a qualified module name, which is more unique and stable than an inferred name.
=== Module URIs
Modules are loaded from _module URIs_.
By default, the following URI types are available for import:
==== File URI:
Example: `+file:///path/to/my_module.pkl+`
Represents a module located on a file system.
==== HTTP(S) URI:
Example: `+https://example.com/my_module.pkl+`
Represents a module imported via an HTTP(S) GET request.
NOTE: Modules loaded from HTTP(S) URIs are only cached until the `pkl` command exits or the `Evaluator` object is closed.
[[module-path-uri]]
==== Module path URI:
Example: `+modulepath:/path/to/my_module.pkl+`
Module path URIs are resolved relative to the _module path_, a search path for modules similar to Java's class path (see the `--module-path` CLI option).
For example, given the module path `/dir1:/zip1.zip:/jar1.jar`, module `+modulepath:/path/to/my_module.pkl+` will be searched for in the following locations:
. `/dir1/path/to/my_module.pkl`
. `/path/to/my_module.pkl` within `/zip1.zip`
. `/path/to/my_module.pkl` within `/jar1.jar`
When evaluating Pkl code from Java, `+modulepath:/path/to/my_module.pkl+` corresponds to class path location `path/to/my_module.pkl`.
In a typical Java project, this corresponds to file path `src/main/resources/path/to/my_module.pkl` or `src/test/resources/path/to/my_module.pkl`.
[[package-asset-uri]]
==== Package asset URI:
Example: `+package://example.com/mypackage@1.0.0#/my_module.pkl+`
Represent a module within a _package_.
A package is a shareable archive of modules and resources that are published to the internet.
To import `package://example.com/mypackage@1.0.0#/my_module.pkl`, Pkl follows these steps:
1. Make an HTTPS GET request to `\https://example.com/mypackage@1.0.0` to retrieve the package's metadata.
2. From the package metadata, download the referenced zip archive, and validate its checksum.
3. Resolve path `/my_module.pkl` within the package's zip archive.
A package asset URI has the following form:
----
'package://' <host> <path> '@' <semver> '#' <asset path>
----
Optionally, the SHA-256 checksum of the package can also be specified:
[source]
----
'package://' <host> <path> '@' <semver> '::sha256:' <sha256-checksum> '#' <asset path>
----
Packages can be managed as dependencies within a _project_.
For more details, consult the <<projects,project>> section of the language reference.
Packages can also be downloaded from a mirror.
For more details, consult the <<mirroring_packages>> section of the language reference.
==== Standard Library URI
Example: `+pkl:math+`
Standard library modules are named `pkl.<simpleName>` and have module URIs of the form `pkl:<simpleName>`.
For example, module `pkl.math` has module URI `pkl:math`.
See the link:{uri-pkl-stdlib-docs-index}[API Docs] for the complete list of standard library modules.
==== Relative URIs
Relative module URIs are interpreted relative to the URI of the enclosing module.
For example, a module with URI `modulepath:/animals/birds/pigeon.pkl`
can import `modulepath:/animals/birds/parrot.pkl`
with `import "parrot.pkl"` or `import "/animals/birds/parrot.pkl"`.
[NOTE]
.Paths on Windows
====
Relative paths use the `/` character as the directory separator on all platforms, including Windows.
Paths that contain drive letters (e.g. `C:`) must be declared as an absolute file URI, for example: `import "file:///C:/path/to/my/module.pkl"`. Otherwise, they are interpreted as a URI scheme.
====
NOTE: When importing a relative directory or file that starts with `@`, the import string must be prefixed with `./`.
Otherwise, this syntax will be interpreted as xref:dependency-notation[dependency notation].
[#dependency-notation]
==== Dependency notation URIs
Example: `+@birds/bird.pkl+`
Dependency notation URIs represent a path within a <<project-dependencies,project or package dependency>>.
For example, import `@birds/bird.pkl` represents path `/bird.pkl` in a dependency named "birds".
A dependency is either a remote package or a local project dependency.
==== Extension points
Pkl embedders can register additional module loaders that recognize other types of module URIs.
==== Evaluation
Module URIs can be evaluated directly:
[source,shell]
----
$ pkl eval path/to/mymodule.pkl
$ pkl eval file:///path/to/my_module.pkl
$ pkl eval https://apple.com/path/to/mymodule.pkl
$ pkl eval --module-path=/pkl-modules modulepath:/path/to/my_module.pkl
$ pkl eval pkl:math
----
[[triple-dot-module-uris]]
==== Triple-dot Module URIs
To simplify referencing ancestor modules in a hierarchical module structure,
relative file and module path URIs may start with `++...++/`, a generalization of `../`.
Module URI `++...++/foo/bar/baz.pkl` resolves to the first existing module among
`../foo/bar/baz.pkl`, `../../foo/bar/baz.pkl`, `../../../foo/bar/baz.pkl`, and so on.
Furthermore, module URI `++...++` is equivalent to `++...++/<currentFileName>`.
Using triple-dot module URIs never resolve to the current module.
For example, a module at path `foo/bar.pkl` that references module URI `++...++/foo/bar.pkl`
does not resolve to itself.
[[module-amend]]
=== Amending a Module
Recall how an object is amended:
[source%tested,{pkl}]
----
pigeon {
name = "Pigeon"
diet = "Seeds"
}
parrot = (pigeon) { // <1>
name = "Parrot" // <2>
}
----
<1> Object `parrot` amends object `pigeon`, inheriting all of its members.
<2> `parrot` overrides `name`.
Amending a module works in the same way, except that the syntax differs slightly:
.pigeon.pkl
[source%tested,{pkl}]
----
name = "Pigeon"
diet = "Seeds"
----
.parrot.pkl
[source%parsed,{pkl}]
----
amends "pigeon.pkl" // <1>
name = "Parrot" // <2>
----
<1> Module `parrot` amends module `pigeon`, inheriting all of its members.
<2> `parrot` overrides `name`.
A module is amended by way of an _amends clause_, which consists of the keyword `amends` followed by the <<Module URIs,module URI>> of the module to amend.
An amends clause comes after the module clause (if present) and before any import clauses:
.parrot.pkl
[source%parsed,{pkl}]
----
module parrot
amends "pigeon.pkl"
import "other.pkl"
name = "Parrot"
----
At most one amends clause is permitted.
A module cannot have both an amends clause and an extends clause.
An amending module has the same type (that is, module class) as the module it amends.
As a consequence, it cannot define new properties, methods, or classes, unless they are declared as `local`.
In our example, this means that module `parrot` can only define (and thus override) the property `name`.
Spelling mistakes such as `namw` are caught immediately, rather than accidentally defining a new property.
Amending is used to fill in _template modules_:
. The template module defines which properties exist, their types, and what module output is desired (for example JSON indented with two spaces).
. The amending module fills in property values as required, relying on the structure, defaults and validation provided by the template module.
. The amending module is evaluated to produce the final result.
Template modules are often provided by third parties and served over HTTPS.
[[module-extend]]
=== Extending a Module
Recall how a class is extended:
.PigeonAndParrot.pkl
[source%tested,{pkl}]
----
open class Pigeon { // <1>
name = "Pigeon"
diet = "Seeds"
}
class Parrot extends Pigeon { // <2>
name = "Parrot" // <3>
diet = "Berries" // <4>
extinct = false // <5>
function say() = "Pkl is great!" // <6>
}
----
<1> Class `Pigeon` is declared as `open` for extension.
<2> Class `Parrot` extends `Pigeon`, inheriting all of its members.
<3> `Parrot` overrides `name`.
<4> `Parrot` overrides `diet`.
<5> `Parrot` defines a new property named `extinct`.
<6> `Parrot` defines a new function named `say`.
Extending a module works in the same way, except that the syntax differs slightly:
.pigeon.pkl
[source%tested,{pkl}]
----
open module pigeon // <1>
name = "Pigeon"
diet = "Seeds"
----
<1> Module `pigeon` is declared as `open` for extension.
.parrot.pkl
[source%parsed,{pkl}]
----
extends "pigeon.pkl" // <1>
name = "Parrot" // <2>
diet = "Berries" // <3>
extinct = false // <4>
function say() = "Pkl is great!" // <5>
----
<1> Module `parrot` extends module `pigeon`, inheriting all of its members.
<2> `parrot` overrides `name`.
<3> `parrot` overrides `diet`.
<4> `parrot` defines a new property named `extinct`.
<5> `parrot` defines a new function named `say`.
A module is extended by way of an _extends clause_, which consists of the keyword `extends` followed by the <<Module URIs,module URI>> of the module to extend.
The extends clause comes after the module clause (if present) and before any import clauses.
Only modules declared as `open` can be extended.
.parrot.pkl
[source%parsed,{pkl}]
----
module parrot
extends "pigeon.pkl"
import "other.pkl"
name = "Parrot"
diet = "Berries"
extinct = false
function say() = "Pkl is great!"
----
At most one extends clause is permitted.
A module cannot have both an amends clause and an extends clause.
Extending a module implicitly defines a new module class that extends the original module's class.
[[import-module]]
=== Importing a Module
A module import makes the imported module accessible to the importing module.
A module is imported by way of either an <<import-clause,import clause>>, or an <<import-expression,import expression>>.
[[import-clause]]
==== Import Clauses
An import clause consists of the keyword `import` followed by the <<Module URIs,module URI>> of the module to import.
An import clause comes after module, amends and extends clauses (if present), and before the module body:
.parrot.pkl
[source%parsed,{pkl}]
----
module parrot
amends "pigeon.pkl"
import "module1.pkl"
import "module2.pkl"
name = "Parrot"
----
Multiple import clauses are permitted.
A module import implicitly defines a new `const local` property through which the imported module can be accessed.
(Remember that modules are regular objects.)
The name of this property, called _import name_, is constructed from the module URI as follows:
. Strip the URI scheme, including the colon (`:`).
. Strip everything up to and including the last forward slash (`/`).
. Strip any trailing `.pkl` file extension.
Here are some examples:
.Local file import
[source%parsed,{pkl}]
----
import "modules/pigeon.pkl" // relative to current module
name = pigeon.name
----
.HTTPS import
[source%parsed,{pkl}]
----
import "https://mycompany.com/modules/pigeon.pkl"
name = pigeon.name
----
.Standard library import
[source%parsed,{pkl}]
----
import "pkl:math"
pi = math.Pi
----
.Package import
[source%parsed,{pkl}]
----
import "package://example.com/birds@1.0.0#/sparrow.pkl"
name = sparrow.name
----
Because its members are automatically visible in every module, the `pkl:base` module is typically not imported.
Occasionally, the default import name for a module may not be convenient or appropriate:
* If not a valid identifier, the import name needs to be enclosed in backticks on each use, for example, `&#96;my-module&#96;.someMember`.
* The import name may clash with other names in the importing module.
In such a case, a different import name can be chosen:
.parrot.pkl
[source%parsed,{pkl}]
----
import "pigeon.pkl" as piggy
name = "Parrot"
diet = piggy.diet
----
[TIP]
.What makes a good module file name?
====
When creating a new module, especially one intended for import into other modules, try to choose a module file name that makes a good import name:
* short
+
Less than six characters, not counting the `.pkl` file extension, is a good rule of thumb.
* valid identifier
+
Stick to alphanumeric characters. Use an underscore (`_`) instead of a hyphen (`-`) as a name separator.
* descriptive
+
An import name should make sense on its own and when used in qualified member names.
====
[[import-expression]]
==== Import Expressions (`import()`)
An import expression consists of the keyword `import`, followed by a <<Module URIs,module URI>> wrapped in parentheses:
[source,{pkl}]
----
module birds
pigeon = import("pigeon.pkl")
parrot = import("parrot.pkl")
----
Unlike import clauses, import expressions only import a value, and do not import a type.
A type is a name that can be used in type positions, for example, as a type annotation.
[[globbed-imports]]
=== Globbed Imports
Multiple modules may be imported at once with `import*`.
When importing multiple modules, a glob pattern is used to match against existing resources.
A globbed import evaluates to a `Mapping`, where keys are the expanded form of the glob and values are import expressions on each individual module.
Globbed imports can be expressed as either a clause or as an expression.
When expressed as a clause, they follow the same naming rules as a normal <<import-clause,import clause>>: they introduce a local property equal to the last path segment without the `.pkl` extension.
A globbed import clause cannot be used as a type.
[source,{pkl}]
----
import* "birds/*.pkl" as allBirds // <1>
import* "reptiles/*.pkl" <2>
birds = import*("birds/*.pkl") // <3>
----
<1> Globbed import clause
<2> Globbed import clause without an explicit name (will import the name `*`)
<3> Globbed import expression
Assuming that a file system contains these files:
[source,txt]
----
.
├── birds/
│ ├── pigeon.pkl
│ ├── parrot.pkl
│ └── falcon.pkl
└── index.pkl
----
The following two snippets are logically identical:
.index.pkl
[source,{pkl}]
----
birds = import*("birds/*.pkl")
----
.index.pkl
[source,{pkl}]
----
birds = new Mapping {
["birds/pigeon.pkl"] = import("birds/pigeon.pkl")
["birds/parrot.pkl"] = import("birds/parrot.pkl")
["birds/falcon.pkl"] = import("birds/falcon.pkl")
}
----
By default, only the `file` and `package` schemes are globbable.
Globbing another scheme will cause Pkl to throw.
Pkl can be extended to provide custom globbable schemes through the link:{uri-pkl-core-ModuleKey}[ModuleKey]
SPI.
When globbing within <<package-asset-uri,packages>>, only the asset path (the fragment section) is globbable.
Otherwise, characters are interpreted verbatim, and not treated as glob wildcards.
For details on how glob patterns work, refer to <<glob-patterns>> in the Advanced Topics section.
NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. +
This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled.
=== Security Checks
When attempting to directly evaluate a module, as in `pkl eval myModule.pkl`,
the following security checks are performed:
* The module URI is checked against the module allowlist (`--allowed-modules`).
The module allowlist is a comma-separated list of regular expressions.
For access to be granted, at least one regular expression must match a prefix of the module URI.
For example, the allowlist `file:,https:` grants access to any module whose URI starts with `file:` or `https:`.
When a module attempts to load another module (via `amends`, `extends`, or `imports`),
the following security checks are performed:
* The target module URI is checked against the module allowlist (`--allowed-modules`).
* The source and target modules' _trust levels_ are determined and compared.
For access to be granted, the source module's trust level must be greater than or equal to the target module's trust level.
By default, there are five trust levels, listed from highest to lowest:
. `repl:` modules (code evaluated in the REPL)
. `file:` modules
. `modulepath:` modules
. All other modules (for example `https:`)
. `pkl:` modules (standard library)
For example, this means that `file:` modules can import `https:` modules, but not the other way around.
If a module URI is resolved in multiple steps, all URIs are subject to the above security checks.
An example of this is an HTTPS URL that results in a redirect.
Pkl embedders can further customize security checks.
[[module-output]]
=== Module Output
By default, the output of evaluating a module is the entire module rendered as Pcf.
There are two ways to change this behavior:
1. _Outside_ the language, by using the `--format` CLI option or the `outputFormat` Gradle task property.
2. _Inside_ the language, by configuring a module's `output` property.
==== CLI
Given the following module:
[source%tested,{pkl}]
.config.pkl
----
a = 10
b {
c = 20
}
----
`pkl eval config.pkl`, which is shorthand for `pkl eval --format pcf config.pkl`, renders the module as Pcf:
[source,{pkl}]
----
a = 10
b {
c = 20
}
----
`pkl eval --format yaml config.pkl` renders the module as YAML:
[source,yaml]
----
a: 10
b:
c: 20
----
Likewise, `pkl eval --format json config.pkl` renders the module as JSON.
[[in-language]]
==== In-language
Now let's do the same -- and more -- inside the language.
Modules have an link:{uri-stdlib-baseModule}/Module#output[output] property that controls what the module's output is and how that output is rendered.
To control *what* the output is, set the link:{uri-stdlib-baseModule}/ModuleOutput#value[output.value] property:
[source%parsed,{pkl}]
----
a = 10
b {
c = 20
}
output {
value = b // defaults to `outer`, which is the entire module
}
----
This produces:
[source,{pkl}]
----
c = 20
----
To control _how_ the output is rendered, set the link:{uri-stdlib-baseModule}/ModuleOutput#renderer[output.renderer] property:
[source%parsed,{pkl}]
----
a = 10
b {
c = 20
}
output {
renderer = new YamlRenderer {}
}
----
The standard library provides these renderers:
* link:{uri-stdlib-baseModule}/JsonRenderer[JsonRenderer]
* link:{uri-stdlib-jsonnetModule}/Renderer[jsonnet.Renderer]
* link:{uri-stdlib-baseModule}/PcfRenderer[PcfRenderer]
* link:{uri-stdlib-baseModule}/PListRenderer[PListRenderer]
* link:{uri-stdlib-baseModule}/PropertiesRenderer[PropertiesRenderer]
* link:{uri-stdlib-protobufModule}/Renderer[protobuf.Renderer]
* link:{uri-stdlib-xmlModule}/Renderer[xml.Renderer]
* link:{uri-stdlib-baseModule}/YamlRenderer[YamlRenderer]
To render a format that is not yet supported, you can implement your own renderer by extending the class link:{uri-stdlib-baseModule}/ValueRenderer[ValueRenderer].
The standard library renderers can be configured with _value converters_, which influence how particular values are rendered.
For example, since YAML does not have a standard way to represent data sizes, a plain `YamlRenderer` cannot render `DataSize` values.
However, we can teach it to:
[source%parsed,{pkl}]
----
quota {
memory = 100.mb
disk = 20.gb
}
output {
renderer = new YamlRenderer {
converters {
[DataSize] = (size) -> "\(size.value) \(size.unit)"
}
}
}
----
This produces:
[source,yaml]
----
quota:
memory: 100 MB
disk: 20 GB
----
In addition to _class_-based converters, renderers also support _path_-based converters:
[source%parsed,{pkl}]
----
output {
renderer = new YamlRenderer {
converters {
["quota.memory"] = (size) -> "\(size.value) \(size.unit)"
["quota.disk"] = (size) -> "\(size.value) \(size.unit)"
}
}
}
----
For more on path-based converters, see {uri-stdlib-PcfRenderer-converters}[PcfRenderer.converters].
Sometimes it is useful to directly compute the final module output, bypassing `output.value` and `output.converters`.
To do so, set the link:{uri-stdlib-baseModule}/ModuleOutput#text[output.text] property to a String value:
[source%parsed,{pkl}]
----
output {
// defaults to `renderer.render(value)`
text = "this is the final output".toUpperCase()
}
----
This produces:
[source]
----
THIS IS THE FINAL OUTPUT
----
[[multiple-file-output]]
==== Multiple File Output
It is sometimes desirable for a single module to produce multiple output files.
This is possible by configuring a module's link:{uri-stdlib-outputFiles}[`output.files`] property
// suppress inspection "AsciiDocLinkResolve"
and specifying the xref:pkl-cli:index.adoc#multiple-file-output-path[`--multiple-file-output-path`]
(or `-m` for short) CLI option.
Here is an example that produces a JSON and a YAML file:
.birds.pkl
[source%parsed,{pkl}]
----
pigeon {
name = "Pigeon"
diet = "Seeds"
}
parrot {
name = "Parrot"
diet = "Seeds"
}
output {
files {
["birds/pigeon.json"] {
value = pigeon
renderer = new JsonRenderer {}
}
["birds/parrot.yaml"] {
value = parrot
renderer = new YamlRenderer {}
}
}
}
----
Running `pkl eval -m output/ birds.pkl` produces the following output files:
.output/birds/pigeon.json
[source,json]
----
{
"name": "Pigeon",
"diet": "Seeds"
}
----
.output/birds/parrot.yaml
[source,yaml]
----
name: Parrot
diet: Berries
----
Within `output.files`,
a key determines a file's path relative to `--multiple-file-output-path`,
and a value determines the file's contents.
If a file's path resolves to a location outside `--multiple-file-output-path`,
evaluation fails with an error.
Non-existing parent directories are created.
[[aggregating-module-outputs]]
===== Aggregating Module Outputs
A value within `output.files` can be another module's `output`.
With this, a module can aggregate the outputs of multiple other modules.
Here is an example:
.pigeon.pkl
[source%parsed,{pkl}]
----
name = "Pigeon"
diet = "Seeds"
output {
renderer = new JsonRenderer {}
}
----
.parrot.pkl
[source%parsed,{pkl}]
----
name = "Parrot"
diet = "Seeds"
output {
renderer = new YamlRenderer {}
}
----
.birds.pkl
[source%parsed,{pkl}]
----
import "pigeon.pkl"
import "parrot.pkl"
output {
files {
["birds/pigeon.json"] = pigeon.output
["birds/parrot.yaml"] = parrot.output
}
}
----
[TIP]
====
When aggregating module outputs,
the appropriate file extensions can be obtained programmatically:
.birds.pkl
[source%parsed,{pkl}]
----
import "pigeon.pkl"
import "parrot.pkl"
output {
files {
["birds/pigeon.\(pigeon.output.renderer.extension)"] = pigeon.output
["birds/parrot.\(parrot.output.renderer.extension)"] = parrot.output
}
}
----
====
[[null-values]]
== Null Values
The keyword `null` indicates the absence of a value.
`null` is an instance of link:{uri-stdlib-Null}[Null], a direct subclass of `Any`.
=== Non-Null Operator
The `!!` (non-null) operator asserts that its operand is non-null.
Here are some examples:
[source%tested%error,{pkl}]
----
name = "Pigeon"
nameNonNull = name!! // <1>
name2 = null
name2NonNull = name2!! // <2>
----
<1> result: `"Pigeon"`
<2> result: _Error: Expected a non-null value, but got `null`._
=== Null Coalescing
The `??` (null coalescing) operator fills in a default for a `null` value.
[source,{pkl-expr}]
----
value ?? default
----
The above expression evaluates to `value` if `value` is non-null, and to `default` otherwise.
Here are some examples:
[source%tested,{pkl}]
----
name = "Pigeon"
nameOrParrot = name ?? "Parrot" // <1>
name2 = null
name2OrParrot = name2 ?? "Parrot" // <2>
----
<1> result: `"Pigeon"`
<2> result: `"Parrot"`
[NOTE]
.Default non-null behavior
====
Many languages allow `null` for (almost) every type, but Pkl does not.
Any type can be extended to include `null` by appending `?` to the type.
For example, `parrot: Bird` will always be non-null, but `pigeon: Bird?` could be `null` - and _is_ by default,
if `pigeon` is never amended. This means if you try to coalesce a (non-nullable) typed variable,
the result is always that variables value.
As per our example `parrot ?? pigeon == parrot` always holds,
but `pigeon ?? parrot` could either be `pigeon` or `parrot`,
depending on whether `pigeon` was ever amended with a non-null value.
====
[[null-propagation]]
=== Null Propagation
The `?.` (null propagation) operator provides null-safe access to a member whose receiver may be `null`.
[source,{pkl-expr}]
----
value?.member
----
The above expression evaluates to `value.member` if `value` is non-null, and to `null` otherwise.
Here are some examples:
[source%tested,{pkl}]
----
name = "Pigeon"
nameLength = name?.length // <1>
nameUpper = name?.toUpperCase() // <2>
name2 = null
name2Length = name2?.length // <3>
name2Upper = name2?.toUpperCase() // <4>
----
<1> result: `6`
<2> result: `"PIGEON"`
<3> result: `null`
<4> result: `null`
The `?.` operator is often combined with `??`:
[source%tested,{pkl}]
----
name = null
nameLength = name?.length ?? 0 // <1>
----
<1> result: `0`
=== ifNonNull Method
The link:{uri-stdlib-ifNonNull}[ifNonNull()] method is a generalization of the <<null-propagation,null propagation>> operator.
[source%parsed,{pkl-expr}]
----
name.ifNonNull((it) -> doSomethingWith(it))
----
The above expression evaluates to `doSomethingWith(name)` if `name` is non-null, and to `null` otherwise.
Here are some examples:
[source%tested,{pkl}]
----
name = "Pigeon"
nameWithTitle = name.ifNonNull((it) -> "Dr." + it) // <1>
name2 = null
name2WithTitle = name2.ifNonNull((it) -> "Dr." + it) // <2>
----
<1> result: `"Dr. Pigeon"`
<2> result: `null`
=== NonNull Type Alias
To express that a property can have any type except `Null`, use the `NonNull` <<type-aliases,type alias>>:
[source,{pkl}]
----
x: NonNull
----
[[if-expressions]]
== If Expressions
An `if` expression serves the same role as the ternary operator (`? :`) in other languages.
Every `if` expression must have an `else` branch.
[source%tested,{pkl}]
----
num = if (2 + 2 == 5) 1984 else 42 // <1>
----
<1> result: `42`
[[resources]]
== Resources
Pkl programs can read external resources, such as environment variables or text files.
To read a resource, use a `read` expression:
[source%parsed,{pkl}]
----
path = read("env:PATH")
----
By default, the following resource URI schemes are supported:
env: :: Reads an environment variable.
Result type is `String`.
prop: :: Reads an external property set via the `-p name=value` CLI option.
Result type is `String`.
file: :: Reads a file from the file system.
Result type is link:{uri-stdlib-Resource}[Resource].
http(s): :: Reads an HTTP(S) resource.
Result type is link:{uri-stdlib-Resource}[Resource].
modulepath: :: Reads a resource from the module path (`--module-path`) or JVM class path.
Result type is link:{uri-stdlib-Resource}[Resource].
See <<module-path-uri,Module Path URI>> for further information.
package: :: Reads a resource from a _package_. Result type is link:{uri-stdlib-Resource}[Resource]. See <<package-asset-uri>> for further information.
Relative resource URIs are resolved against the enclosing module's URI.
Resources are cached in memory on the first read.
Therefore, subsequent reads are guaranteed to return the same result.
[[nullable-reads]]
=== Nullable Reads
If a resource does not exist or cannot be read, `read()` fails with an error.
To recover from the absence of a resource, use `read?()` instead,
which returns `null` for absent resources:
[source%parsed,{pkl}]
----
port = read?("env:PORT")?.toInt() ?? 1234
----
[[globbed-reads]]
=== Globbed Reads
Multiple resources may be read at the same time with `read*()`.
When reading multiple resources, a glob pattern is used to match against existing resources.
A globbed read returns a `Mapping`, where the keys are the expanded form of the glob, and values are `read` expressions on each individual resource.
Assuming that a file system contains these files:
[source,txt]
----
.
├── birds/
│ ├── pigeon.pkl
│ ├── parrot.pkl
│ └── falcon.pkl
└── index.pkl
----
The following two snippets are logically identical:
.index.pkl
[source%parsed,{pkl}]
----
birdFiles = read*("birds/*.pkl")
----
.index.pkl
[source%parsed,{pkl}]
----
birdFiles = new Mapping {
["birds/pigeon.pkl"] = read("birds/pigeon.pkl")
["birds/parrot.pkl"] = read("birds/parrot.pkl")
["birds/falcon.pkl"] = read("birds/falcon.pkl")
}
----
By default, the following schemes support globbing:
* `modulepath`
* `file`
* `env`
* `prop`
Globbing other resources results in an error.
For details on how glob patterns work, reference <<glob-patterns>> in the Advanced Topics section.
NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. +
This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled.
[NOTE]
====
The `env` and `prop` schemes are considered opaque, as they do not have traditional hierarchical elements like a host, path, or query string.
While globbing is traditionally viewed as a way to match elements in a file system, a glob pattern is simply a way to match strings.
Thus, environment variables and external properties can be globbed, where their names get matched according to the rules described by the glob pattern.
To match all values within these schemes, use the `+++**+++` wildcard.
This has the effect of matching names that contain a forward slash too (`/`).
For example, the expression `read*("+++env:**+++")` will evaluate to a Mapping of all environment variables.
====
=== Extending resource readers
When Pkl is embedded within another runtime, it can be extended to read other kinds of resources.
When embedded into a JVM application, new resources may be read by implementing the link:{uri-pkl-core-ResourceReader}[ResourceReader] SPI.
When Pkl is embedded within Swift, new resources may be read by implementing the link:{uri-pkl-swift-resource-reader-docs}[ResourceReader] interface.
When Pkl is embedded within Go, new resources may be read by implementing the link:{uri-pkl-go-resource-reader-docs}[ResourceReader] interface.
=== Resource Allowlist
When attempting to read a resource, the resource URI is checked against the resource allowlist (`--allowed-resources`).
In embedded mode, the allowlist is configured via an evaluator's link:{uri-pkl-core-SecurityManager}[SecurityManager].
The resource allowlist is a comma-separated list of regular expressions.
For access to be granted, at least one regular expression must match a prefix of the resource URI.
For example, the allowlist `file:,https:` grants access to any resource whose URI starts with `file:` or `https:`.
[[errors]]
== Errors
By design, errors are fatal in Pkl -- there is no way to recover from them.
To raise an error, use a `throw` expression:
[source%tested%error,{pkl}]
----
myValue = throw("You won't be able to recover from this one!") // <1>
----
<1> `myValue` never receives a value because the program exits.
The error message is printed to the console and the program exits.
In embedded mode, a link:{uri-pkl-core-PklException}[PklException] is thrown.
[[debugging]]
== Debugging
When debugging Pkl code, it can be useful to print the value of an expression.
To do so, use a `trace` expression:
[source%tested,{pkl}]
----
num1 = 42
num2 = 16
res = trace(num1 * num2)
----
Tracing an expression does not affect its result, but prints both its source code and result on standard error:
[source,shell]
----
pkl: TRACE: num1 * num2 = 672 (at file:///some/module.pkl, line 42)
----
[[advanced-topics]]
== Advanced Topics
This section discusses language features that are generally more relevant to template and library authors than template consumers.
<<meaning-of-new,Meaning of `new`>> +
<<let-expressions,Let Expressions>> +
<<type-tests,Type Tests>> +
<<type-casts,Type Casts>> +
<<lists,Lists>> +
<<sets,Sets>> +
<<maps,Maps>> +
<<regular-expressions,Regular Expressions>> +
<<type-aliases,Type Aliases>> +
<<type-annotations,Type Annotations>> +
<<anonymous-functions,Anonymous Functions>> +
<<amend-null,Amending Null Values>> +
<<when-generators,When Generators>> +
<<for-generators,For Generators>> +
<<spread-syntax,Spread Syntax>> +
<<member-predicates,Member Predicates (`[[...]]`)>> +
<<this-keyword,`this` Keyword>> +
<<outer-keyword,`outer` Keyword>> +
<<super-keyword,`super` Keyword>> +
<<module-keyword,`module` Keyword>> +
<<glob-patterns,Glob Patterns>> +
<<doc-comments,Doc Comments>> +
<<name-resolution,Name Resolution>> +
<<reserved-keywords,Reserved Keywords>> +
<<blank-identifiers,Blank Identifiers>> +
<<projects,Projects>> +
<<external-readers,External Readers>> +
<<mirroring_packages,Mirroring packages>>
[[meaning-of-new]]
=== Meaning of `new`
Objects in Pkl always <<amending-objects, amends>> _some_ value.
The `new` keyword is a special case of amending where a contextual value is amended.
In Pkl, there are two forms of `new` objects:
* `new` with explicit type information, for example, `new Foo {}`.
* `new` without type information, for example, `new {}`.
==== Type defaults
To understand instantiation cases without explicit parent or type information, it's important to first understand implicit default values.
When a property is declared in a module or class but is not provided an explicit default value, the property's default value becomes the type's default value.
Similarly, when `Listing` and `Mapping` types are declared with explicit type arguments for their element or value, their `default` property amends that declared type.
When `Listing` and `Mapping` types are declared without type arguments, their `default` property amends an empty `Dynamic` object.
Some types, including `Pair` and primitives like `String`, `Number`, and `Boolean` have no default value; attempting to render such a property results in the error "Tried to read property `<name>` but its value is undefined".
[source,{pkl}]
----
class Bird {
name: String = "polly"
}
bird: Bird // <1>
birdListing: Listing<Bird> // <2>
birdMapping: Mapping<String, Bird> // <3>
----
<1> Without an explicit default value, this property has default value `new Bird { name = "polly" }`
<2> With an explicit element type argument, this property's default value is equivalent to `new Listing<Bird> { default = (_) -> new Bird { name = "polly" } }`
<3> With an explicit value type argument, this property's default value is equivalent to `new Mapping<String, Bird> { default = (_) -> new Bird { name = "polly" } }`
==== Explicitly Typed `new`
Instantiating an object with `new <type>` results in a value that amends the specified type's default value.
Notably, creating a `Listing` element or assigning a `Mapping` entry value with an explicitly typed `new` ignores the object's `default` value.
[source,{pkl}]
----
class Bird {
/// The name of the bird
name: String
/// Whether this is a bird of prey or not.
isPredatory: Boolean?
}
newProperty = new Bird { // <1>
name = "Warbler"
}
someListing = new Listing<Bird> {
default {
isPredatory = true
}
new Bird { // <2>
name = "Sand Piper"
}
}
someMapping = new Mapping<String, Bird> {
default {
isPredatory = true
}
["Penguin"] = new Bird { // <3>
name = "Penguin"
}
}
----
<1> Assigning a `new` explicitly-typed value to a property.
<2> Adding an `new` explicitly-typed `Listing` element.
The value will not have property `isPredatory = true` as the `default` property of the `Listing` is not used.
<3> Assigning a `new` explicitly-typed value to a `Mapping` entry.
The value will not have property `isPredatory = true` as the `default` property of the `Mapping` is not used.
==== Implicitly Typed `new`
When using the implicitly typed `new` invocation, there is no explicit parent value to amend.
In these cases, Pkl infers the amend operation's parent value based on context:
* When assigning to a declared property, the property's default value is amended (<<amend-null, including `null`>>).
If there is no type associated with the property, an empty `Dynamic` object is amended.
* When assigning to an entry (e.g. a `Mapping` member) or element (e.g. a `Listing` member), the enclosing object's `default` property is applied to the corresponding index or key, respectively, to produce the value to be amended.
* In other cases, evaluation fails with the error message "Cannot tell which parent to amend".
The type annotation of a <<methods,method>> parameter is not used for inference.
In this case, the argument's type should be specified explicitly.
[source,{pkl}]
----
class Bird {
name: String
function listHatchlings(items: Listing<String>): Listing<String> = new {
for (item in items) {
"\(name):\(item)"
}
}
}
typedProperty: Bird = new { // <1>
name = "Swift"
}
untypedProperty = new { // <2>
hello = "world"
}
typedListing: Listing<Bird> = new {
new { // <3>
name = "Kite"
}
}
untypedListing: Listing = new {
new { // <4>
hello = "there"
}
}
typedMapping: Mapping<String, Bird> = new {
default { entryKey ->
name = entryKey
}
["Saltmarsh Sparrow"] = new { // <5>
name = "Sharp-tailed Sparrow"
}
}
amendedMapping = (typedMapping) {
["Saltmarsh Sparrow"] = new {} // <6>
}
class Aviary {
birds: Listing<Bird> = new {
new { name = "Osprey" }
}
}
aviary: Aviary = new {
birds = new { // <7>
new { name = "Kiwi" }
}
}
swiftHatchlings = typedProperty.listHatchlings(new { "Poppy"; "Chirpy" }) // <8>
----
<1> Assignment to a property with an explicitly declared type, amending `new Bird {}`.
<2> Assignment to an undeclared property in module context, amending `new Dynamic {}`.
<3> `Listing` element creation, amending implicit `default`, `new Bird {}`.
<4> `Listing` element creation, amending implicit `default`, `new Dynamic {}`.
<5> `Mapping` value assignment, amending the result of applying `default` to `"Saltmarsh Sparrow"`, `new Bird { name = "Saltmarsh Sparrow" }`.
<6> `Mapping` value assignment _replacing_ the parent's entry, amending the result of applying `default` to `"Saltmarsh Sparrow"`, `new Bird { name = "Saltmarsh Sparrow" }`.
<7> Amending the property default value `new Listing { new Bird { name = "Osprey" } }`; the result contains both birds.
<8> Error: Cannot tell which parent to amend.
[[let-expressions]]
=== Let Expressions
A `let` expression is Pkl's version of an (immutable) local variable.
Its syntax is:
[source]
----
let (<name> = <value>) <expr>
----
A `let` expression is evaluated as follows:
. `<name>` is bound to `<value>`, itself an expression.
. `<expr>` is evaluated, which can refer to `<value>` by its `<name>` (this is the point).
. The result of <expr> becomes the result of the overall expression.
Here is an example:
[source%tested,{pkl}]
----
birdDiets =
let (diets = List("Seeds", "Berries", "Mice"))
List(diets[2], diets[0]) // <1>
----
<1> result: `birdDiets = List("Mice", "Seeds")`
`let` expressions serve two purposes:
- They introduce a human-friendly name for a potentially complex expression.
- They evaluate a potentially expensive expression that is used in multiple places only once.
`let` expressions can have type annotations:
[source%tested,{pkl}]
----
birdDiets =
let (diets: List<String> = List("Seeds", "Berries", "Mice"))
diets[2] + diets[0] // <1>
----
<1> result: `birdDiets = List("Mice", "Seeds")`
`let` expressions can be stacked:
[source%tested,{pkl}]
----
birdDiets =
let (birds = List("Pigeon", "Barn owl", "Parrot"))
let (diet = List("Seeds", "Mice", "Berries"))
birds.zip(diet) // <1>
----
<1> result: `birdDiets = List(Pair("Pigeon", "Seeds"), Pair("Barn owl", "Mice"), Pair("Parrot", "Berries"))`
[[type-tests]]
=== Type Tests
To test if a value conforms to a type, use the _is_ operator.
All the following tests hold:
[source%tested,{pkl}]
----
test1 = 42 is Int
test2 = 42 is Number
test3 = 42 is Any
test4 = !(42 is String)
open class Base
class Derived extends Base
base = new Base {}
test5 = base is Base
test6 = base is Any
test7 = !(base is Derived)
derived = new Derived {}
test8 = derived is Derived
test9 = derived is Base
test10 = derived is Any
----
A value can be tested against any type, not just a class:
[source,{pkl}]
----
test1 = email is String(contains("@")) // <1>
test2 = map is Map<Int, Base> // <2>
test3 = name is "Pigeon"|"Barn owl"|"Parrot" // <3>
----
<1> `email` is tested for being a string that contains a `@` sign
<2> `map` is tested for being a map from `Int` to `Base` values
<3> `name` is tested for being one of `"Pigeon"`, `"Barn owl"`, or `"Parrot"`
[[type-casts]]
=== Type Casts
The _as_ (type cast) operator performs a runtime type check on its operand.
If the type check succeeds, the operand is returned as-is; otherwise, an error is thrown.
[source%tested,{pkl}]
----
birds {
new { name = "Pigeon" }
new { name = "Barn owl" }
}
names = birds.toList().map((it) -> it.name) as List<String>
----
Although type casts are never mandatory in Pkl, they occasionally help humans and tools better understand an expression's type.
[[lists]]
=== Lists
A value of type link:{uri-stdlib-List}[List] is an ordered, indexed collection of _elements_.
A list's elements have zero-based indexes and are eagerly evaluated.
[TIP]
.When to use List vs. <<listings,Listing>>
====
* When a collection of elements needs to be specified literally, use a listing.
* When a collection of elements needs to be transformed in a way that cannot be achieved by <<amending-listings,amending>> a listing, use a list.
* If in doubt, use a listing.
Templates and schemas should almost always use listings instead of lists.
Note that listings can be converted to lists when the need arises.
====
Lists are constructed with the `List()` methodfootnote:soft-keyword[Strictly speaking, `List`, `Set`, and `Map` are currently soft keywords. The goal is to eventually turn them into regular standard library methods.]:
[source%tested,{pkl}]
----
list1 = List() // <1>
list2 = List(1, 2, 3) // <2>
list3 = List(1, "x", 5.min, List(1, 2, 3)) // <3>
----
<1> result: empty list
<2> result: list of length 3
<3> result: heterogeneous list whose last element is another list
To concatenate lists, use the `+` operator:
[source%tested,{pkl-expr}]
----
List(1, 2) + List(3, 4) + List(5)
----
To access a list element by index, use the `[]` (subscript) operator:
[source%tested,{pkl}]
----
list = List(1, 2, 3, 4)
listElement = list[2] // <1>
----
<1> result: `3`
Class `List` offers a link:{uri-stdlib-List}[rich API].
Here are just a few examples:
[source%tested,{pkl}]
----
list = List(1, 2, 3, 4)
res1 = list.contains(3) // <1>
res2 = list.first // <2>
res3 = list.rest // <3>
res4 = list.reverse() // <4>
res5 = list.drop(1).take(2) // <5>
res6 = list.map((n) -> n * 3) // <6>
----
<1> result: `true`
<2> result: `1`
<3> result: `List(2, 3, 4)`
<4> result: `List(4, 3, 2, 1)`
<5> result: `List(2, 3)`
<6> result: `List(3, 6, 9, 12)`
[[sets]]
=== Sets
A value of type link:{uri-stdlib-Set}[Set] is a collection of unique _elements_.
Sets are constructed with the `Set()` methodfootnote:soft-keyword[]:
[source%tested,{pkl}]
----
res1 = Set() // <1>
res2 = Set(1, 2, 3) // <2>
res3 = Set(1, 2, 3, 1) // <3>
res4 = Set(1, "x", 5.min, List(1, 2, 3)) // <4>
----
<1> result: empty set
<2> result: set of length 3
<3> result: same set of length 3
<4> result: heterogeneous set that contains a list as its last element
Sets retain the order of elements when constructed, which impacts how they are iterated over.
However, this order is not considered when determining equality of two sets.
[source%tested,{pkl}]
----
res1 = Set(4, 3, 2)
res2 = res1.first // <1>
res3 = res1.toListing() // <2>
res4 = Set(2, 3, 4) == res1 // <3>
----
<1> result: `4`
<2> result: `new Listing { 4; 3; 2 }`
<3> result: `true`
To compute the union of sets, use the `+` operator:
[source%tested,{pkl-expr}]
----
Set(1, 2) + Set(2, 3) + Set(5, 3) // <1>
----
<1> result: `Set(1, 2, 3, 5)`
Class `Set` offers a link:{uri-stdlib-Set}[rich API].
Here are just a few examples:
[source%tested,{pkl}]
----
set = Set(1, 2, 3, 4)
res1 = set.contains(3) // <1>
res2 = set.drop(1).take(2) // <2>
res3 = set.map((n) -> n * 3) // <3>
res4 = set.intersect(Set(3, 9, 2)) // <4>
----
<1> result: `true`
<2> result: `Set(2, 3)`
<3> result: `Set(3, 6, 9, 12)`
<4> result: `Set(3, 2)`
[[maps]]
=== Maps
A value of type link:{uri-stdlib-Map}[Map] is a collection of _values_ indexed by _key_.
A map's key-value pairs are called its _entries_.
Keys and values are eagerly evaluated.
[TIP]
.When to use Map vs. <<mappings,Mapping>>
====
* When key-value style data needs to be specified literally, use a mapping.
* When key-value style data needs to be transformed in ways that cannot be achieved by <<amending-mappings,amending>> a mapping, use a map.
* If in doubt, use a mapping.
Templates and schemas should almost always use mappings instead of maps.
(Note that mappings can be converted to maps when the need arises.)
====
Maps are constructed by passing alternating keys and values to the `Map()` methodfootnote:soft-keyword[]:
[source%tested,{pkl}]
----
map1 = Map() // <1>
map2 = Map(1, "one", 2, "two", 3, "three") // <2>
map3 = Map(1, "x", 2, 5.min, 3, Map(1, 2)) // <3>
----
<1> result: empty map
<2> result: set of length 3
<3> result: heterogeneous map whose last value is another map
Any Pkl value can be used as a map key:
[source%tested,{pkl-expr}]
----
Map(new Dynamic { name = "Pigeon" }, 10.gb)
----
Maps retain the order of entries when constructed, which impacts how they are iterated over.
However, this order is not considered when determining equality of two maps.
[source%tested,{pkl}]
----
res1 = Map(2, "hello", 1, "world")
res2 = res1.entries.first // <1>
res3 = res1.toMapping() // <2>
res4 = res1 == Map(1, "world", 2, "hello") // <3>
----
<1> result: `Pair(2, "hello")`
<2> result: `new Mapping { [2] = "hello"; [1] = "world" }`
<3> result: `true`
To merge maps, use the `+` operator:
[source%tested,{pkl}]
----
combinedMaps = Map(1, "one") + Map(2, "two", 1, "three") + Map(4, "four") // <1>
----
<1> result: `Map(1, "three", 2, "two", 4, "four")`
To access a value by key, use the `[]` (subscript) operator:
[source%tested,{pkl}]
----
map = Map("Pigeon", 5.gb, "Parrot", 10.gb)
parrotValue = map["Parrot"] // <1>
----
<1> result: `10.gb`
Class `Map` offers a link:{uri-stdlib-Map}[rich API].
Here are just a few examples:
[source%tested,{pkl}]
----
map = Map("Pigeon", 5.gb, "Parrot", 10.gb)
res1 = map.containsKey("Parrot") // <1>
res2 = map.containsValue(8.gb) // <2>
res3 = map.isEmpty // <3>
res4 = map.length // <4>
res5 = map.getOrNull("Falcon") // <5>
----
<1> result: `true`
<2> result: `false`
<3> result: `false`
<4> result: `2`
<5> result: `null`
[[bytes]]
=== Bytes
A value of type `Bytes` is a sequence of `UInt8` elements.
`Bytes` can be constructed by passing byte values into the constructor.
[source,pkl]
----
bytes1 = Bytes(0xff, 0x00, 0x3f) // <1>
bytes2 = Bytes() // <2>
----
<1> Result: a `Bytes` with 3 elements
<2> Result: an empty `Bytes`
`Bytes` can also be constructed from a base64-encoded string, via `base64DecodedBytes`:
[source,pkl]
----
bytes3 = "cGFycm90".base64DecodedBytes // <1>
----
<1> Result: `Bytes(112, 97, 114, 114, 111, 116)`
==== `Bytes` vs `List<UInt8>`
`Bytes` is similar to `List<UInt8>` in that they are both sequences of `UInt8` elements.
However, they are semantically distinct.
`Bytes` represent binary data, and is typically rendered differently.
For example, they are rendered as `<data>` tags when using `PListRenderer`.
`Bytes` also have different performance characteristics; a value of type `Bytes` tends to be managed as a contiguous memory block.
Thus, they are more compact and consume less memory.
However, they are not optimized for transformations.
For example, given two values of size `M` and `N`, concatenating two `Bytes` values allocates O(M + N) space, whereas concatenating two `List` values allocates O(1) space.
[[regular-expressions]]
=== Regular Expressions
A value of type link:{uri-stdlib-Regex}[Regex] is a regular expression with the same syntax and semantics as a link:{uri-javadoc-Pattern}[Java regular expression].
Regular expressions are constructed with the link:{uri-stdlib-Regex-method}[Regex()] method:
[source%tested,{pkl}]
----
emailRegex = Regex(#"([\w\.]+)@([\w\.]+)"#)
----
// note: first \ on next line is asciidoc escape
Notice the use of custom string delimiters `\#"` and `"#`, which change the string's escape character from `\` to `\#`.
As a consequence, the regular expression's backslash escape character no longer requires escaping.
To test if a string fully matches a regular expression, use link:{uri-stdlib-matches}[String.matches()]:
[source%tested,{pkl-expr}]
----
"pigeon@example.com".matches(emailRegex)
----
Many `String` methods accept either a `String` or `Regex` argument.
Here is an example:
[source%tested,{pkl}]
----
res1 = "Pigeon<pigeon@example.com>".contains("pigeon@example.com")
res2 = "Pigeon<pigeon@example.com>".contains(emailRegex)
----
To find all matches of a regex in a string, use link:{uri-stdlib-Regex-match}[Regex.findMatchesIn()].
The result is a list of link:{uri-stdlib-RegexMatch}[RegexMatch] objects containing details about each match:
[source%tested,{pkl}]
----
matches = emailRegex.findMatchesIn("pigeon@example.com / falcon@example.com / parrot@example.com")
list1 = matches.drop(1).map((it) -> it.start) // <1>
list2 = matches.drop(1).map((it) -> it.value) // <2>
list3 = matches.drop(1).map((it) -> it.groups[1].value) // <3>
----
<1> result: `List(0, 19, 40)` (the entire match, `matches[0]`, was dropped)
<2> result: `List("pigeon@example.com", "falcon@example.com", "parrot@example.com")`
<3> result: `List("pigeon, falcon, parrot")`
[[type-aliases]]
=== Type Aliases
A _type alias_ introduces a new name for a (potentially complicated) type:
[source%tested,{pkl}]
----
typealias EmailAddress = String(matches(Regex(#".+@.+"#)))
----
Once a type alias has been defined, it can be used in type annotations:
[source%tested,{pkl}]
----
email: EmailAddress = "pigeon@example.com"
emailList: List<EmailAddress> = List("pigeon@example.com", "parrot@example.com")
----
New type aliases can be defined in terms of existing ones:
[source%tested,{pkl}]
----
typealias EmailList = List<EmailAddress>
emailList: EmailList = List("pigeon@example.com", "parrot@example.com")
----
Type aliases can have type parameters:
[source%tested,{pkl}]
----
typealias StringMap<Value> = Map<String, Value>
map: StringMap<Int> = Map("Pigeon", 42, "Falcon", 21)
----
Code generators have different strategies for dealing with type aliases:
* the xref:java-binding:codegen.adoc[Java] code generator inlines them
* the xref:kotlin-binding:codegen.adoc[Kotlin] code generator turns them into Kotlin type aliases.
Type aliases for unions of <<String Literal Types>> are turned into enum classes by both code generators.
[[predefined-type-aliases]]
==== Predefined Type Aliases
The _pkl.base_ module defines the following type aliases:
* link:{uri-stdlib-Int8}[Int8] (-128 to 127)
* link:{uri-stdlib-Int16}[Int16] (-32,768 to 32,767)
* link:{uri-stdlib-Int32}[Int32] (-2,147,483,648 to 2,147,483,647)
//-
* link:{uri-stdlib-UInt8}[UInt8] (0 to 255)
* link:{uri-stdlib-UInt16}[UInt16] (0 to 65,535)
* link:{uri-stdlib-UInt32}[UInt32] (0 to 4,294,967,295)
* link:{uri-stdlib-UInt}[UInt] (0 to 9,223,372,036,854,775,807)
//-
* link:{uri-stdlib-Uri}[Uri] (any String value)
WARNING: Note that `UInt` has the same maximum value as `Int`, half of what would normally be expected.
The main purpose of the provided integer aliases is to enforce the range of an integer:
[source%tested%error,{pkl}]
----
port: UInt16 = -1
----
This gives:
[source,shell,subs="quotes"]
----
Type constraint *isBetween(0, 65535)* violated.
Value: -1
----
To restrict a number to a custom range, use the link:{uri-stdlib-isBetween}[isBetween] method:
[source%tested,{pkl}]
----
port: Int(isBetween(0, 1023)) = 443
----
NOTE: Remember that numbers are always instances of `Int` or `Float`.
Type aliases such as `UInt16` only check that numbers are within a certain range.
The xref:java-binding:codegen.adoc[Java] and xref:kotlin-binding:codegen.adoc[Kotlin] code generators
map predefined type aliases to the most suitable Java and Kotlin types.
For example, `UInt8` is mapped to `java.lang.Byte` and `kotlin.Byte`, and `Uri` is mapped to `java.net.URI`.
[[type-annotations]]
=== Type Annotations
Property and method definitions may optionally contain type annotations.
Type annotations serve the following purposes:
* Documentation
+ Type annotations help to document data models. They are included in any generated documentation.
* Validation
+ Type annotations are validated at runtime.
* Defaults
+ Type-annotated properties have <<default-values,Default Values>>.
* Code Generation
+ Type annotations enable statically typed access to configuration data through code generation.
* Tooling
+ Type annotations enable advanced tooling features such as code completion in editors.
==== Class Types
Any class can be used as a type:
[source%parsed,{pkl}]
----
class Bird {
name: String // <1>
}
bird: Bird // <2>
----
<1> Declares an instance property of type `String`.
<2> Declares a module property of type `Bird`.
==== Module Types
Any module import can be used as type:
[source%parsed,{pkl}]
.bird.pkl
----
name: String
lifespan: Int
----
[source%parsed,{pkl}]
.birds.pkl
----
import "bird.pkl"
pigeon: bird // <1>
parrot: bird // <1>
----
<1> Guaranteed to amend _bird.pkl_.
As a special case, the `module` keyword denotes the _enclosing_ module's type:
[source%parsed,{pkl}]
.bird.pkl
----
name: String
lifespan: Int
friends: Listing<module>
----
[source%parsed,{pkl}]
.pigeon.pkl
----
amends "bird.pkl"
name = "Pigeon"
lifespan = 8
friends {
import("falcon.pkl") // <1>
}
----
<1> _falcon.pkl_ (not shown here) is guaranteed to amend _bird.pkl_.
==== Type Aliases
Any <<type-aliases,type alias>> can be used as a type:
[source%parsed,{pkl}]
----
typealias EmailAddress = String(contains("@"))
email: EmailAddress // <1>
emailList: List<EmailAddress> // <2>
----
<1> equivalent to `email: String(contains("@"))` for type checking purposes
<2> equivalent to `emailList: List<String(contains("@"))>` for type checking purposes
==== Nullable Types
Class types such as `Bird` (see above) do not admit `null` values.
To turn them into _nullable types_, append a question mark (`?`):
[source%parsed,{pkl}]
----
bird: Bird = null // <1>
bird2: Bird? = null // <2>
----
<1> throws `Type mismatch: Expected a value of type Bird, but got null`
<2> succeeds
The only class types that admit `null` values despite not ending in `?` are `Any` and `Null`.
(`Null` is not very useful as a type because it _only_ admits `null` values.)
`Any?` and `Null?` are equivalent to `Any` and `Null`, respectively.
In some languages, nullable types are also known as _optional types_.
[[generic-types]]
==== Generic Types
The following class types are _generic types_:
* `Pair`
* `Collection`
* `Listing`
* `List`
* `Mapping`
* `Set`
* `Map`
* `Function0`
* `Function1`
* `Function2`
* `Function3`
* `Function4`
* `Function5`
* `Class`
A generic type has constituent types written in angle brackets (`<>`):
[source%parsed,{pkl}]
----
pair: Pair<String, Bird> // <1>
coll: Collection<Bird> // <2>
list: List<Bird> // <3>
set: Set<Bird> // <4>
map: Map<String, Bird> // <5>
mapping: Mapping<String, Bird> // <6>
----
<1> a pair `String` and `Bird` as types for the first and second element, respectively
<2> a collection of `Bird` elements
<3> a list of `Bird` elements
<4> a set of `Bird` elements
<5> a map with `String` keys and `Bird` values
<6> a mapping of `String` keys and `Bird` values
Omitting the constituent types is equivalent to declaring them as `unknown`:
[source%parsed,{pkl}]
----
pair: Pair // equivalent to `Pair<unknown, unknown>`
coll: Collection // equivalent to `Collection<unknown>`
list: List // equivalent to `List<unknown>`
set: Set // equivalent to `Set<unknown>`
map: Map // equivalent to `Map<unknown, unknown>`
mapping: Mapping // equivalent to `Mapping<unknown, unknown>`
----
The `unknown` type is both a top and a bottom type.
When a static type analyzer encounters an expression of `unknown` type,
it backs off and trusts the user that they know what they are doing.
[[union-types]]
==== Union Types
A value of type `A | B`, read "A or B", is either a value of type `A` or a value of type `B`.
[source%tested,{pkl}]
----
class Bird { name: String }
bird1: String|Bird = "Pigeon"
bird2: String|Bird = new Bird { name = "Pigeon" }
----
More complex union types can be formed:
[source%parsed,{pkl}]
----
foo: List<Boolean|Number|String>|Bird
----
Union types have no implicit default values, but an explicit type can be chosen using a `*` marker:
[source%parsed,{pkl}]
----
foo: "a"|"b" // undefined. Will throw an error if not amended
bar: "a"|*"b" // default value will be taken from type "b"
baz: "a"|"b" = "a" // explicit value is given
qux: String|*Int // default taken from Int, but Int has no default. Will throw if not amended
----
Union types often come in handy when writing schemas for legacy JSON or YAML files.
[[string-literal-types]]
==== String Literal Types
A string literal type admits a single string value:
[source%parsed,{pkl}]
----
diet: "Seeds"
----
While occasionally useful on their own,
string literal types are often combined with <<Union Types>> to form enumerated types:
[source%parsed,{pkl}]
----
diet: "Seeds"|"Berries"|"Insects"
----
To reuse an enumerated type, introduce a type alias:
[source%parsed,{pkl}]
----
typealias Diet = "Seeds"|"Berries"|"Insects"
diet: Diet
----
The Java and Kotlin code generators turn type aliases for enumerated types into enum classes.
[[nothing-type]]
==== Nothing Type
The `nothing` type is the bottom type of Pkl's type system, the counterpart of top type `Any`.
The bottom type is assignment-compatible with every other type, and no other type is assignment-compatible with it.
Being assignment-compatible with every other type may sound too good to be true, but there is a catch -- the `nothing` type has no values!
Despite being a lonely type, `nothing` has practical applications.
For example, it is used in the standard library's `TODO()` method:
[source%tested,{pkl}]
----
function TODO(): nothing = throw("TODO")
----
A `nothing` return type indicates that a method never returns normally but always throws an error.
[[unknown-type]]
==== Unknown Type
The `unknown` type footnote:[Also known as _dynamic type_. We do not use that term to avoid confusion with `Dynamic`, Pkl's dynamic object type.] is ``nothing``'s even stranger cousin: it is both a top and bottom type!
This makes `unknown` assignment-compatible with every other type, and every other type assignment-compatible with `unknown`.
When a static type analyzer encounters a value of `unknown` type,
it backs off and trusts the code's author to know what they are doing -- for example, whether a method called on the value exists.
==== Progressive Disclosure
In the spirit of link:{uri-progressive-disclosure}[progressive disclosure], type annotations are optional in Pkl.
Omitting a type annotation is equivalent to specifying the type `unknown`:
[source%parsed,{pkl}]
----
lifespan = 42 // <1>
map: Map // <2>
function say(name) = name // <3>
----
<1> shorthand for `lifespan: unknown = 42`
(As a dynamically typed language, Pkl does not try to statically infer types.)
<2> shorthand for `map: Map<unknown, unknown> = Map()`
<3> shorthand for `function say(name: unknown): unknown = name`
[[default-values]]
==== Default Values
Type-annotated properties have implicit "empty" default values depending on their type:
[source%tested,{pkl}]
----
class Bird
coll: Collection<Bird> // = List() <1>
list: List<Bird> // = List() <2>
set: Set<Bird> // = Set() <3>
map: Map<String, Bird> // = Map() <4>
listing: Listing<Bird> // = new Listing { default = (index) -> new Bird {} } <5>
mapping: Mapping<String, Bird> // = new Mapping { default = (key) -> new Bird {} } <6>
obj: Bird // = new Bird {} <7>
nullable: Bird? // = Null(new Bird {}) <8>
union: *Bird|String // = new Bird {} <9>
stringLiteral: "Pigeon" // = "Pigeon" <10>
nullish: Null // = null <11>
----
<1> Properties of type `Collection` default to the empty list.
<2> Properties of type `List` default to the empty list.
<3> Properties of type `Set` default to the empty set.
<4> Properties of type `Map` default to the empty map.
<5> Properties of type `Listing<X>` default to an empty listing whose default element is the default for `X`.
<6> Properties of type `Mapping<X, Y>` default to an empty mapping whose default value is the default for `Y`.
<7> Properties of non-external class type `X` default to `new X {}`.
<8> Properties of type `X?` default to `Null(x)` where `x` is the default for `X`.
<9> Properties with a union type have no default value. By prefixing one of the types in a union with a `*`, the default of that type is chosen as the default for the union.
<10> Properties with a string literal type default to the type's only value.
<11> Properties of type `Null` default to `null`.
See <<amend-null, Amending Null Values>> for further information.
Properties of the following types do not have implicit default values:
* `abstract` classes, including `Any` and `NotNull`
* Union types, unless an explicit default is given by prefixing one of the types with `*`.
* `external` (built-in) classes, including:
** `String`
** `Boolean`
** `Int`
** `Float`
** `Duration`
** `DataSize`
** `Pair`
** `Regex`
Accessing a property that neither has an (implicit or explicit) default value nor has been overridden throws an error:
[source%tested%error,{pkl}]
----
name: String
----
[[type-constraints]]
==== Type Constraints
A type may be followed by a comma-separated list of _type constraints_ enclosed in round brackets (`()`).
A type constraint is a boolean expression that must hold for the annotated element.
Type constraints enable advanced runtime validation that goes beyond the capabilities of static type checking.
[source%tested,{pkl}]
----
class Bird {
name: String(length >= 3) // <1>
parent: String(this != name) // <2>
}
pigeon: Bird = new {
name = "Pigeon"
parent = "Pigeon Sr." // <3>
}
----
<1> Restricts `name` to have at least three characters.
<2> The name of the bird (`this`) should not be the same as the name of the `parent`.
<3> Note how `parent` is different from `name`. If they were the same, we would be thrown a constraint error.
In the following example, we define a `Bird` with a name of only two characters.
[source%tested%error,{pkl}]
----
pigeon: Bird = new {
// fails the constraint because [name] is less than 3 characters
name = "Pi"
}
----
Boolean expressions are convenient for ad-hoc type constraints.
Alternatively, type constraints can be given as lambda expressions accepting a single argument, namely the value to be validated.
This allows for the abstraction and reuse of type constraints.
[source%tested,{pkl}]
----
class Project {
local emailAddress = (str) -> str.matches(Regex(#".+@.+"#))
email: String(emailAddress)
}
project: Project = new {
email = "projectPigeon@example.com"
}
----
[source%tested%error,{pkl}]
----
project: Project = new {
// fails the constraint because `"projectPigeon-example.com"` doesn't match the regular expression.
email = "projectPigeon-example.com"
}
----
===== Composite Type Constraints
A composite type can have type constraints for the overall type, its constituent types, or both.
[source%tested,{pkl}]
----
class Project {
local emailAddress = (str) -> str.matches(Regex(#".+@.+"#))
// constrain the nullable type's element type
type: String(contains("source"))?
// constrain the map type and its key/value types
contacts: Map<String(!isEmpty), String(emailAddress)>(length <= 5)
}
project: Project = new {
type = "open-source"
contacts = Map("Pigeon", "pigeon@example.com")
}
----
[[anonymous-functions]]
=== Anonymous Functions
An _anonymous function_ is a function without a name.
Most modern general-purpose programming languages support anonymous functions,
under names such as _lamba expressions_, _arrow functions_, _function literals_, _closures_, or _procs_.
Anonymous functions have their own literal syntax:
[source]
----
() -> expr // <1>
(param) -> expr // <2>
(param1, param2, ..., paramN) -> expr // <3>
----
<1> Zero-parameter lambda expression
<2> Single-parameter lambda expression
<3> Multi-parameter lambda expression
Here is an example:
[source%tested,{pkl-expr}]
----
(n) -> n * 3
----
This anonymous function accepts a parameter named `n`, multiplies it by 3, and returns the result.
Anonymous functions are values of type link:{uri-stdlib-Function}[Function], more specifically
link:{uri-stdlib-Function0}[Function0], link:{uri-stdlib-Function1}[Function1],
link:{uri-stdlib-Function2}[Function2], link:{uri-stdlib-Function3}[Function3],
link:{uri-stdlib-Function4}[Function4], or link:{uri-stdlib-Function5}[Function5].
They cannot have more than five parameters.
To invoke an anonymous function, call its link:{uri-stdlib-Function1-apply}[apply] method:
[source%tested,{pkl-expr}]
----
((n) -> n * 3).apply(4) // 12
----
Many standard library methods accept anonymous functions:
[source%tested,{pkl-expr}]
----
List(1, 2, 3).map((n) -> n * 3) // List(3, 6, 9)
----
Anonymous functions can be assigned to properties, thereby giving them a name:
[source%tested,{pkl}]
----
add = (a, b) -> a + b
added = add.apply(2, 3)
----
[TIP]
====
If an anonymous function is not intended to be passed as value, it is customary to declare a method instead:
[source%tested,{pkl}]
----
function add(a, b) = a + b
added = add(2, 3)
----
====
An anonymous function's parameters can have type annotations:
[source%tested,{pkl-expr}]
----
(a: Number, b: Number) -> a + b
----
Applying this function to arguments not of type `Number` results in an error.
Anonymous functions are _closures_: They can access members defined in a lexically enclosing scope, even after leaving that scope:
[source%tested,{pkl}]
----
a = 42
addToA = (b: Number) -> a + b
list = List(1, 2, 3).map(addToA) // List(43, 44, 45)
----
Single-parameter anonymous functions can also be applied with the `|>` (pipe) operator,
which expects a function argument to the left and an anonymous function to the right.
The pipe operator works especially well for chaining multiple functions:
[source%tested,{pkl}]
----
mul3 = (n) -> n * 3
add2 = (n) -> n + 2
num = 4
|> mul3
|> add2
|> mul3 // <1>
----
<1> result: `42`
Like methods, anonymous functions can be recursive:
[source%tested,{pkl}]
----
factor = (n: Number(isPositive)) -> if (n < 2) n else n * factor.apply(n - 1)
num = factor.apply(5) // <1>
----
<1> result: `120`
[[mixins]]
==== Mixins
A mixin is an anonymous function used to apply the same modification to different objects.
Even though mixins are regular functions, they are best created with object syntax:
[source%tested,{pkl}]
----
withDiet = new Mixin {
diet = "Seeds"
}
----
Mixins can optionally specify which type of object they apply to:
[source%tested,{pkl}]
----
class Bird { diet: String }
withDietTyped = new Mixin<Bird> {
diet = "Seeds"
}
----
For properties with type annotation, the shorthand `new { ... }` syntax can be used:
[source%tested,{pkl}]
----
withDietTyped: Mixin<Bird> = new {
diet = "Seeds"
}
----
To apply a mixin, use the `|>` (pipe) operator:
[source%tested,{pkl}]
----
pigeon {
name = "Pigeon"
}
pigeonWithDiet = pigeon |> withDiet
barnOwl {
name = "Barn owl"
}
barnOwlWithDiet = barnOwl |> withDiet
----
`withDiet` can be generalized by turning it into a factory method for mixins:
[source%tested,{pkl}]
----
function withDiet(_diet: String) = new Mixin {
diet = _diet
}
seedPigeon = pigeon |> withDiet("Seeds")
MiceBarnOwl = barnOwl |> withDiet("Mice")
----
Mixins can themselves be modified with <<function-amending,function amending>>.
[[function-amending]]
==== Function Amending
An anonymous function that returns an object can be amended with the same syntax as that object.
The result is a new function that accepts the same number of parameters as the original function,
applies the original function to them, and amends the returned object.
Function amending is a special form of function composition.
Thanks to function amending, link:{uri-stdlib-Listing-default}[Listing.default]
and link:{uri-stdlib-Mapping-default}[Mapping.default] can be treated as if they were objects,
only gradually revealing their true (single-parameter function) nature:
[source%tested,{pkl}]
----
birds = new Mapping {
default { // <1>
diet = "Seeds"
}
["Pigeon"] { // <2>
lifespan = 8
}
}
----
<1> Amends the `default` function, which returns a default mapping value given a mapping key, and sets property `diet`.
<2> Implicitly applies the amended `default` function and amends the returned object with property `lifespan`.
The result is a mapping whose entry `"Pigeon"` has both `diet` and `lifespan` set.
When amending an anonymous function, it is possible to access its parameters
by declaring a comma-separated, arrow (`->`) terminated parameter list after the opening curly brace (`{`).
Once again, this is especially useful to configure a listing's or mapping's `default` function:
[source%tested,{pkl}]
----
birds = new Mapping {
default { key -> // <1>
name = key
}
["Pigeon"] {} // <2>
["Barn owl"] {} // <3>
}
----
<1> Amends the `default` function and sets the `name` property to the mapping entry's key.
To access the `default` function's key parameter, it is declared with `key ->`.
(Any other parameter name could be chosen, but `key` is customary for default functions.)
<2> Defines a mapping entry with key `"Pigeon"`
<3> Defines a mapping entry with key `"Barn owl"`
The result is a mapping with two entries `"Pigeon"` and `"Barn owl"` whose `name` properties are set to their keys.
Function amending can also be used to refine <<mixins,mixins>>.
[[amend-null]]
=== Amending Null Values
It's time to lift a secret: The predefined `null` value is just one of the potentially many values of type `Null`.
First, here are the technical facts:
* Null values are constructed with `pkl.base#Null()`.
* `Null(x)` constructs a null value that is equivalent to `x` when amended.
In other words, `Null(x) { ... }` is equivalent to `x { ... }`.
* All null values are equal according to `==`.
We say that `Null(x)` is a "null value with default x".
But what is it useful for?
====
Null values with default are used to define properties that are null ("switched off") by default but have a default value once amended ("switched on").
====
Here is an example:
.template.pkl
[source%tested,{pkl}]
----
// we don't have a pet yet, but already know that it is going to be a bird
pet = Null(new Dynamic {
animal = "bird"
})
----
[source%parsed,{pkl}]
----
amends "template.pkl"
// We got a pet, let's fill in its name
pet {
name = "Parry the Parrot"
}
----
A null value can be switched on without adding or overriding a property:
[source%parsed,{pkl}]
----
amends "template.pkl"
// We do not need to name anything if we have no pet yet
pet {}
----
The predefined `null` value is defined as `Null(new Dynamic {})`.
In other words, amending `null` is equivalent to amending `Dynamic {}` (the empty dynamic object):
[source%tested,{pkl}]
----
pet = null
----
[source%tested,{pkl}]
----
pet {
name = "Parry the Parrot"
}
----
In most cases, the `Null()` method is not used directly.
Instead, it is used under the hood to create implicit defaults for properties with nullable type:
.template.pkl
[source%tested,{pkl}]
----
class Pet {
name: String
animal: String = "bird"
}
// defaults to `Null(Pet {})`
pet: Pet?
----
[source%tested,{pkl}]
----
amends "template.pkl"
pet {
name = "Perry the Parrot"
}
----
The general rule is: A property with nullable type `X?` defaults to `Null(x)` if type `X` has default value `x`, and to `null` if `X` has no default value.
[[when-generators]]
=== When Generators
`when` generators conditionally generate object members.
They come in two variants:
. `when (<condition>) { <members> }`
. `when (<condition>) { <members> } else { <members> }`
The following code conditionally generates properties `hobby` and `idol`:
[source%tested,{pkl}]
----
isSinger = true
parrot {
lifespan = 20
when (isSinger) {
hobby = "singing"
idol = "Frank Sinatra"
}
}
----
`when` generators can have an `else` part:
[source%tested,{pkl}]
----
isSinger = false
parrot {
lifespan = 20
when (isSinger) {
hobby = "singing"
idol = "Aretha Franklin"
} else {
hobby = "whistling"
idol = "Wolfgang Amadeus Mozart"
}
}
----
Besides properties, `when` generators can generate elements and entries:
[source%tested,{pkl}]
----
abilities {
"chirping"
when (isSinger) {
"singing" // <1>
}
"whistling"
}
abilitiesByBird {
["Barn owl"] = "hooing"
when (isSinger) {
["Parrot"] = "singing" // <2>
}
["Parrot"] = "whistling"
}
----
<1> conditional element
<2> conditional entry
[[for-generators]]
=== For Generators
`for` generators generate object members in a loop.
They come in two variants:
. `for (<value> in <iterable>) { <members> }`
. `for (<key>, <value> in <iterable>) { <members> }`
The following code generates a `birds` object containing three elements.
Each element is an object with properties `name` and `lifespan`.
[source%tested,{pkl}]
----
names = List("Pigeon", "Barn owl", "Parrot")
birds {
for (_name in names) {
new {
name = _name
lifespan = 42
}
}
}
----
The following code generates a `birdsByName` object containing three entries.
Each entry is an object with properties `name` and `lifespan` keyed by name.
[source%tested,{pkl}]
----
namesAndLifespans = Map("Pigeon", 8, "Barn owl", 15, "Parrot", 20)
birdsByName {
for (_name, _lifespan in namesAndLifespans) {
[_name] {
name = _name
lifespan = _lifespan
}
}
}
----
The following types are iterable:
|===
|Type |Key |Value
|`IntSeq`
|element index (`Int`)
|element value (`Int`)
|`List<Element>`
|element index (`Int`)
|element value (`Element`)
|`Set<Element>`
|element index (`Int`)
|element value (`Element`)
|`Map<Key, Value>`
|entry key (`Key`)
|entry value (`Value`)
|`Bytes`
|element index (`Int`)
|element value (`UInt8`)
|`Listing<Element>`
|element index (`Int`)
|element value (`Element`)
|`Mapping<Key, Value>`
|entry key (`Key`)
|entry value (`Value`)
|`Dynamic`
|element index (`Int`) +
entry key +
property name (`String`)
|element value +
entry value +
property value
|===
Indices are zero-based.
Note that `for` generators can generate elements and entries but not properties.footnote:[More precisely, they cannot generate properties with a non-constant name.]
[[spread-syntax]]
=== Spread Syntax (`\...`)
Spread syntax generates object members from an iterable value.
There are two variants of spread syntax, a non-nullable variant and a nullable variant.
1. `\...<iterable>`
2. `\...?<iterable>`
Spreading an xref:objects[`Object`] (one of `Dynamic`, `Listing` and `Mapping`) will unpack all of its members into the enclosing object footnote:[Values that are xref:typed-objects[`Typed`] are not iterable.].
Entries become entries, elements become elements, and properties become properties.
[source%tested,{pkl}]
----
entries1 {
["Pigeon"] = "Piggy the Pigeon"
["Barn owl"] = "Barney the Barn owl"
}
entries2 {
...entries1 // <1>
}
elements1 { 1; 2 }
elements2 {
...elements1 // <2>
}
properties1 {
name = "Pigeon"
diet = "Seeds"
}
properties2 {
...properties1 // <3>
}
----
<1> Spreads entries `["Pigeon"] = "Piggy the Pigeon"` and `["Barn owl"] = "Barney the Barn owl"`
<2> Spreads elements `1` and `2`
<3> Spreads properties `name = "Pigeon"` and `diet = "Seeds"`
Spreading all other iterable types generates members determined by the iterable.
The following table describes how different iterables turn into object members:
|===
|Iterable type|Member type
| `Map`
| Entry
| `List`
| Element
| `Set`
| Element
| `IntSeq`
| Element
| `Bytes`
| Element
|===
These types can only be spread into enclosing objects that support that member type.
For example, a `List` can be spread into a `Listing`, but cannot be spread into a `Mapping`.
In some ways, spread syntax can be thought of as a shorthand for a xref:for-generators[for generator]. One key difference is that spread syntax can generate properties, which is not possible with a for generator.
[NOTE]
====
Look out for duplicate key conflicts when using spreads.
Spreading entries or properties may cause conflicts due to matched existing key definitions.
In the following code snippet, `"Pigeon"` is declared twice in the `newPets` object, and thus is an error.
[source%tested%error,{pkl}]
----
oldPets {
["Pigeon"] = "Piggy the Pigeon"
["Parrot"] = "Perry the Parrot"
}
newPets {
...oldPets
["Pigeon"] = "Toby the Pigeon" // <1>
}
----
<1> Error: Duplicate definition of member `"Pigeon"`.
====
==== Nullable spread
A non-nullable spread (`\...`) will error if the value being spread is `null`.
In contrast, a nullable spread (`\...?`) is syntactic sugar for wrapping a spread in a xref:when-generators[`when`].
The following two snippets are logically identical.
[source%parsed,{pkl}]
----
result {
...?myValue
}
----
[source%parsed,{pkl}]
----
result {
when (myValue != null) {
...myValue
}
}
----
[[member-predicates]]
=== Member Predicates (`[[...]]`)
Occasionally it is useful to configure all object members matching a predicate.
This is especially true when configuring elements, which—unlike entries—cannot be accessed by key:
[source%tested,{pkl}]
----
environmentVariables { // <1>
new { name = "PIGEON"; value = "pigeon-value" }
new { name = "PARROT"; value = "parrot-value" }
new { name = "BARN OWL"; value = "barn-owl-value" }
}
updated = (environmentVariables) {
[[name == "PARROT"]] { // <2>
value = "new-value" // <3>
}
}
----
<1> a listing of environment variables
<2> amend element(s) whose name equals "PARROT" +
(`name` is shorthand for `this.name`)
<3> update value to "new-value"
The predicate, enclosed in double brackets (`\[[...]]`), is matched against each member of the enclosing object.
Within the predicate, `this` refers to the member that the predicate is matched against.
Matching members are amended (`{ ... }`) or overridden (`= <new-value>`).
[[this-keyword]]
=== `this` keyword
Normally, the `this` keyword references the enclosing object's receiver.
Example:
[source,pkl]
----
bird {
eatsInsects = this is InsectavorousBird
}
----
When used inside a <<type-constraints,type constraint>>, `this` refers to the value being tested.
Example:
[source,pkl]
----
port: UInt16(this > 1000)
----
When used inside a <<member-predicates,member predicate>>, `this` refers to the value being matched against.
Example:
[source,pkl]
----
animals {
[[this is Bird]] {
canFly = true
}
}
----
[[receiver]]
==== Receiver
The receiver is the bottom-most object in the <<prototype-chain>>.
That means that, within the context of an amending object, the reciever is the amending object.
Example:
[source,pkl]
----
hidden lawyerBird {
title = "\(this.name), Esq."
}
polly = (lawyerBird) {
name = "Polly" // <1>
}
----
<1> Polly has title `"Polly, Esq."`.
[[outer-keyword]]
=== `outer` keyword
The `outer` keyword references the <<receiver,receiver>> of the immediately outer lexical object.
It can be useful to disambiguate a lookup that might otherwise resolve elsewhere.
Example:
[source%tested,pkl]
----
foo {
bar = "bar"
qux {
bar = outer.bar // <1>
}
}
----
<1> References `bar` one level higher.
Note that `outer` cannot be chained.
In order to reference a value more than one level higher, a typical pattern is to declare a local property at that level.
For example:
[source%parsed,pkl]
----
foo {
local self = this
bar {
baz {
qux = self.qux
}
}
}
----
[[super-keyword]]
=== `super` keyword
The `super` keyword references the parent object in the <<prototype-chain,prototype chain>>.
When used within a class, it refers to the superclass's prototype.
When used within an object, it refers to the parent object in the amends chain.
Example:
[source%tested,pkl]
----
bird = new { name = "Quail" }
bird2 = (bird) { name = "Ms. \(super.name)" } // <1>
abstract class Bird {
foods: Listing<String>
function canEat(food: String): Boolean = foods.contains(food)
}
class InsectavorousBird extends Bird {
function canEat(food: String) =
super.canEat(food) || food == "insect" // <2>
}
----
<1> Result: `"Ms. Quail"`
<2> Calls parent class method `canEat()`
The `super` keyword must be followed by property/method access, or subscript.
`super` by itself a syntax error; whereas `super.foo` and `super["foo"]` are valid expressions.
[[module-keyword]]
=== `module` keyword
The `module` keyword can be used as either a value, or as a type.
When used as a value, it refers to the <<receiver,receiver>> of the module itself.
[source%tested,pkl]
----
name = "Quail"
some {
deep {
object {
name = module.name // <1>
}
}
}
----
<1> Resolves to `"Quail"`
When used as a type, it is the module's class.
[source%parsed,pkl]
----
module Bird
friend: module // <1>
----
<1> Is class `Bird`
The `module` type is a _self type_.
If the module is extended by another module, the `module` type refers to the extending module when
in the context of that module.
[[glob-patterns]]
=== Glob Patterns
Resources and modules may be imported at the same time by globbing with the <<globbed-imports>> and <<globbed-reads>> features.
Pkl's glob patterns mostly follow the rules described by link:{uri-glob-7}[glob(7)], with the following differences:
* `*` includes names that start with a dot (`.`).
* `+++**+++` behaves like `*`, except it also matches directory boundary characters (`/`).
* Named character classes are not supported.
* Collating symbols are not supported.
* Equivalence class expressions are not supported.
* Support for <<glob-sub-patterns,sub-patterns>> (patterns within `{` and `}`) are added.
Here is a full specification of how globs work:
==== Wildcards
The following tokens denote wildcards:
[cols="1,2"]
|===
|Wildcard |Meaning
|`*`
|Match zero or more characters, until a directory boundary (`/`) is reached.
|`**`
|Match zero or more characters, crossing directory boundaries.
|`?`
|Match a single character.
|`[...]`
|Match a single character represented by this <<character-classes,character class>>.
|===
NOTE: Unlike globs within shells, the `*` wildcard includes names that start with a dot (`.`).
[[character-classes]]
==== Character Classes
Character classes are sequences delimited by the `[` and `]` characters, and represent a single
character as described by the sequence within the enclosed brackets.
For example, the pattern `[abc]` means "a single character that is a, b, or c".
Character classes may be negated using `!`.
For example, the pattern `[!abc]` means "a single character that is not a, b, nor c".
Character classes may use the `-` character to denote a range.
The pattern `[a-f]` is equivalent to `[abcdef]`.
If the `-` character exists at the beginning or the end of a character class, it does not carry any special meaning.
Within a character class, the characters `{`, `}`, `\`, `*`, and `?` do not have any special meaning.
A character class is not allowed to be empty.
Thus, if the first character within the character class is `]`, it is treated literally and not as the closing delimiter of the character class.
For example, the glob pattern `[]abc]` matches a single character that is either `]`, `a`, `b`, or `c`.
[[glob-sub-patterns]]
==== Sub-patterns
Sub-patterns are glob patterns delimited by the `{` and `}` characters, and separated by the `,` character.
For example, the pattern `{pigeon,parrot}` will match either `pigeon` or `parrot`.
Sub-patterns cannot be nested. The pattern `{foo,{bar,baz}}` is not a valid glob pattern, and an error will be thrown during evaluation.
==== Escapes
The escape character (`\`) can be used to remove the special meaning of a character. The following escapes are valid:
* `\[`
* `\*`
* `\?`
* `\\`
* `\{`
All other escapes are considered a syntax error and an error is thrown.
TIP: If incorporating escape characters into a glob pattern, use <<custom-string-delimiters,custom string delimiters>> to express the glob pattern. For example, `+++import*(#"\{foo.pkl"#)+++`. This way, the backslash is interpreted as a backslash and not a string escape.
==== Examples
[cols="1,2"]
|===
|Pattern |Description
|`*.pk[lg]`
|Anything suffixed by `.pkl`, or `.pkg`.
|`**.y{a,}ml`
|Anything suffixed by either `yml` or `yaml`, crossing directory boundaries.
|`birds/{\*.yml,*.json}`
|Anything within the `birds` subdirectory that ends in `.yml` or `.json`. This pattern is equivalent to `birds/*.{yml,json}`.
|`a?*.txt`
|Anything starting with `a` and at least one more letter, and suffixed with `.txt`.
|`modulepath:/**.pkl`
|All Pkl files in the module path.
|===
[[quoted-identifiers]]
=== Quoted Identifiers
An identifier is the name part of an entity in Pkl.
Entities that are named by identifiers include classes, properties, typealiases, and modules.
For example, `class Bird` has the identifier `Bird`.
Normally, an identifier must conform to Unicode's {uri-unicode-identifier}[UAX31-R1-1 syntax], with the additions of `_` and `$` permitted as identifier start characters.
Additionally, an identifier cannot clash with a keyword.
To define an identifier that is otherwise illegal, enclose them in backticks.
This is called a _quoted identifier_.
[source,{pkl}]
----
`A Bird's First Flight Time` = 5.s
----
[NOTE]
====
Backticks are not part of a quoted identifier's name, and surrounding an already legal identifier with backticks is redundant.
[source,{pkl}]
----
`number` = 42 // <1>
res1 = `number` // <2>
res2 = number // <3>
----
<1> Equivalent to `number = 42`
<2> References property `{backtick}number{backtick}`
<3> Also references property `{backtick}number{backtick}`
====
[[doc-comments]]
=== Doc Comments
Doc comments are the user-facing documentation of a module and its members.
They consist of one or more lines starting with a triple slash (`///`).
Here is a doc comment for a module:
[source%tested,{pkl}]
----
/// An aviated animal going by the name of [bird](https://en.wikipedia.org/wiki/Bird).
///
/// These animals live on the planet Earth.
module com.animals.Birds
----
Doc comments are written in Markdown.
The following Markdown features are supported:
* all link:{uri-common-mark}[CommonMark] features
* https://help.github.com/articles/organizing-information-with-tables[GitHub flavored Markdown tables]
[NOTE]
====
Plaintext URLs are only rendered as links when enclosed in angle brackets:
[source,{pkl}]
----
/// A link is *not* generated for https://example.com.
/// A link *is* generated for <https://example.com>.
----
====
Doc comments are consumed by humans reading source code, the _Pkldoc_ documentation generator, code generators, and editor/IDE plugins.
They are programmatically accessible via the link:{uri-stdlib-reflectModule}/[pkl.reflect] Pkl API and link:{uri-pkl-core-ModuleSchema}[ModuleSchema] Java API.
[TIP]
.Doc Comment Style Guidelines
====
* Use proper spelling and grammar.
* Start each sentence on a new line and capitalize the first letter.
* End each sentence with a punctuation mark.
* The first paragraph of a doc comment is its _summary_.
Keep the summary short (a single sentence is common)
and insert an empty line (`///`) before the next paragraph.
====
Doc comments can be attached to module, class, type alias, property, and method declarations.
Here is a comprehensive example:
.Birds.pkl
[source%tested,{pkl}]
----
/// An aviated animal going by the name of [bird](https://en.wikipedia.org/wiki/Bird).
///
/// These animals live on the planet Earth.
module com.animals.Birds
/// A bird living on Earth.
///
/// Has [name] and [lifespan] properties and an [isOlderThan()] method.
class Bird {
/// The name of this bird.
name: String
/// The lifespan of this bird.
lifespan: UInt8
/// Tells if this bird is older than [bird].
function isOlderThan(bird: Bird): Boolean = lifespan > bird.lifespan
}
/// An adult [Bird].
typealias Adult = Bird(lifespan >= 2)
/// A common [Bird] found in large cities.
pigeon: Bird = new {
name = "Pigeon"
lifespan = 8
}
/// Creates a [Bird] with the given [_name] and lifespan `0`.
function Infant(_name: String): Bird = new { name = _name; lifespan = 0 }
----
[[member-links]]
==== Member Links
To link to a member declaration, write the member's name enclosed in square brackets (`[]`):
[source,{pkl}]
----
/// A common [Bird] found in large cities.
----
To customize the link text, insert the desired text, enclosed in square brackets, before the member name:
[source,{pkl}]
----
/// A [common Bird][Bird] found in large cities.
----
Custom link text can use markup:
[source,{pkl}]
----
/// A [*common* Bird][Bird] found in large cities.
----
The short link `[Bird]` is equivalent to `[{backtick}Bird{backtick}][Bird]`.
Member links are resolved according to Pkl's normal name resolution rules.
The syntax for linking to the members of _Birds.pkl_ (see above) is as follows:
Module::
* `[module]` (from same module)
* `[Birds]` (from a module that contains `import "Birds.pkl"`)
Class::
* `[Bird]` (from same module)
* `[Birds.Bird]` (from a module that contains `import "Birds.pkl"`)
Type Alias::
* `[Adult]` (from same module)
* `[Birds.Adult]` (from a module that contains `import "Birds.pkl"`)
Class Property::
* `[name]` (from same class)
* `[Bird.name]` (from same module)
* `[Birds.Bird.name]` (from a module that contains `import "Birds.pkl"`)
Class Method::
* `[greet()]` (from same class)
* `[Bird.greet()]` (from same module)
* `[Birds.Bird.greet()]` (from a module that contains `import "Birds.pkl"`)
Class Method Parameter::
* `[bird]` (from same method)
Module Property::
* `[pigeon]` (from same module)
* `[Birds.pigeon]` (from a module that contains `import "Birds.pkl"`)
Module Method::
* `[isPigeon()]` (from same module)
* `[Birds.isPigeon()]` (from a module that contains `import "Birds.pkl"`)
Module Method Parameter::
* `[bird]` (from same method)
Members of `pkl.base` can be linked to by their simple name:
[source,{pkl}]
----
/// Returns a [String].
----
Module-level members can be prefixed with `module.` to resolve name conflicts:
[source,{pkl}]
----
/// See [module.pigeon].
----
To exclude a member from documentation and code completion, annotate it with `@Unlisted`:
[source%parsed,{pkl}]
----
@Unlisted
pigeon: Bird
----
The following member links are marked up as code but not rendered as links:footnote:[Only applies to links without custom link text.]
* `[null]`, `[true]`, `[false]`, `[this]`, `[unknown]`, `[nothing]`
* self-links
* subsequent links to the same member from the same doc comment
* links to a method's own parameters
Nevertheless, it is a good practice to use member links in the above cases.
[[name-resolution]]
=== Name Resolution
Consider this snippet of code buried deep inside a config file:
[source%parsed,{pkl}]
----
a = x + 1
----
The call site's "variable" syntax reveals that `x` refers to a _LAMP_
(let binding, anonymous function parameter, method parameter, or property) definition. But which one?
To answer this question, Pkl follows these steps:
. Search the lexically enclosing scopes of `x`,
starting with the scope in which `x` occurs and continuing outwards
up to and including the enclosing module's top-level scope, for a LAMP definition named `x`.
If a match is found, this is the answer.
. Search the `pkl.base` module for a top-level definition of property `x`.
If a match is found, this is the answer.
. Search the <<prototype-chain,prototype chain>> of `this`, from bottom to top, for a definition of property `x`.
If a match is found, this is the answer.
. Throw a "name `x` not found" error.
NOTE: Pkl's LAMP name resolution is inspired by link:{uri-newspeak}[Newspeak].
The goal is for name resolution to be stable with regard to changes in external modules.
This is why lexically enclosing scopes are searched before the prototype chain of `this`,
and why the prototype chains of lexically enclosing scopes are not searched,
which sometimes requires the use of `outer.` or `module.`.
For name resolution to fully stabilize, the list of top-level properties defined in `pkl.base` needs to be freezed.
This is tentatively planned for Pkl 1.0.
Consider this snippet of code buried deep inside a config file:
[source%parsed,{pkl}]
----
a = x("foo") + 1
----
The call site's method call syntax reveals that `x` refers to a method definition. But which one?
To answer this question, Pkl follows these steps:
. Search the call sites' lexically enclosing scopes,
starting with the scope in which the call site occurs and continuing outwards
up to and including the enclosing module's top-level scope, for a definition of method `x`.
If a match is found, this is the answer.
. Search the `pkl.base` module for a top-level definition of method `x`.
If a match is found, this is the answer.
. Search the class inheritance chain of `this`, starting with the class of `this`
and continuing upwards until and including class `Any`, for a method named `x.`
If a match is found, this is the answer.
. Throw a "method `x` not found" error.
NOTE: Pkl does not support arity or type-based method overloading.
Hence, the argument list of a method call is irrelevant for method resolution.
[[prototype-chain]]
==== Prototype Chain
Pkl's object model is based on link:{uri-prototypical-inheritance}[prototypical inheritance].
The prototype chain of objectfootnote:[An instance of `Listing`, `Mapping`, `Dynamic`, or (a subclass of) `Typed`.] `x` contains, from bottom to top:
1. The chain of objects amended to create `x`, ending in `x` itself, in reverse order.footnote:[All objects in this chain are instances of the same class,
except when a direct conversion between listing, mapping, dynamic, and typed object has occurred.
For example, `Typed.toDynamic()` returns a dynamic object that amends a typed object.]
2. The prototype of the class of the top object in (1).
If no amending took place, this is the class of `x`.
3. The prototypes of the superclasses of (2).
The prototype of class `X` is an instance of `X` that defines the defaults for properties defined in `X`.
Its direct ancestor in the prototype chain is the prototype of the superclass of `X`.
The prototype of class `Any` sits at the top of every prototype chain.
To reduce the chance of naming collisions, `Any` does not define any property names.footnote:[Method resolution searches the class inheritance rather than prototype chain.]
Consider the following code:
[source%tested,{pkl}]
----
one = new Dynamic { name = "Pigeon" }
two = (one) { lifespan = 8 }
----
The prototype chain of object `two` contains, now listed from top to bottom:
. The prototype of class `Any`.
. The prototype of class `Dynamic`.
. `one`
. `two`
Consider the following code:
[source%tested,{pkl}]
----
abstract class Named {
name: String
}
class Bird extends Named {
lifespan: Int = 42
}
one = new Bird { name = "Pigeon" }
two = (one) { lifespan = 8 }
----
The prototype chain of object `two` contains, listed from top to bottom:
. The prototype of class `Any`.
. The prototype of class `Typed`.
. The prototype of class `Named`.
. The prototype of class `Bird`.
. `one`
. `two`
===== Non-object Values
The prototype chain of non-object value `x` contains, from bottom to top:
1. The prototype of the class of `x`.
2. The prototypes of the superclasses of (1).
For example, the prototype chain of value `42` contains, now listed from top to bottom:
. The prototype of class `Any`.
. The prototype of class `Number`.
. The prototype of class `Int`.
A prototype chain never contains a non-object value, such as `42`.
[[reserved-keywords]]
=== Reserved keywords
The following keywords are reserved in the language.
They cannot be used as a regular identifier, and currently do not have any meaning.
* `protected`
* `override`
* `record`
* `delete`
* `case`
* `switch`
* `vararg`
To use these names in an identifier, <<quoted-identifiers, surround them with backticks>>.
For a complete list of keywords, consult field `Lexer.KEYWORDS` in {uri-github-PklLexer}[Lexer.java].
[[blank-identifiers]]
=== Blank Identifiers
Blank identifiers can be used in many places to ignore parameters and variables. +
`_` is not a valid identifier. To use it as a parameter or variable name,
it needs to be enclosed in backticks: +`_`+.
==== Functions and methods
[source%tested,{pkl}]
----
birds = List("Robin", "Swallow", "Eagle", "Falcon")
indexes = birds.mapIndexed((i, _) -> i)
function constantly(_, second) = second
----
==== For generators
[source%tested,{pkl}]
----
birdColors = Map("Robin", "blue", "Eagle", "white", "Falcon", "red")
birds = new Listing {
for (name, _ in birdColors) {
name
}
}
----
==== Let bindings
[source%tested,{pkl}]
----
name = let (_ = trace("defining name")) "Eagle"
----
==== Object bodies
[source%tested,{pkl}]
----
birds = new Dynamic {
default { _ ->
species = "Bird"
}
["Falcon"] {}
["Eagle"] {}
}
----
[[projects]]
=== Projects
A _project_ is a directory of Pkl modules and other resources.
It is defined by the presence of a `PklProject` file that amends the standard library module
`pkl:Project`.
Defining a project serves the following purposes:
1. It allows defining common evaluator settings for Pkl modules within a logical project.
2. It helps with managing <<package-asset-uri,package>> dependencies for Pkl modules within a logical project.
3. It enables packaging and sharing the contents of the project as a <<package-asset-uri,package>>.
4. It allows importing packages via dependency notation.
[[project-dependencies]]
==== Dependencies
A project is useful for managing <<package-asset-uri,package>> dependencies.
Within a PklProject file, dependencies can be defined:
.PklProject
[source%tested,{pkl}]
----
amends "pkl:Project"
dependencies {
["birds"] { // <1>
uri = "package://example.com/birds@1.0.0"
}
}
----
<1> Declare dependency on `package://example.com/birds@1.0.0` with simple name "birds".
These dependencies can then be imported by their simple name.
This syntax is called _dependency notation_.
Example:
[source,{pkl}]
----
import "@birds/Bird.pkl" // <1>
pigeon: Bird = new {
name = "Pigeon"
}
----
<1> Dependency notation; imports path `/Bird.pkl` within dependency `package://example.com/birds@1.0.0`
NOTE: Internally, Pkl assigns URI scheme `projectpackage` to project dependencies imported using dependency notation.
When the project gets published as a _package_, these names and URIs are preserved as the package's dependencies.
[[resolving-dependencies]]
==== Resolving Dependencies
Dependencies that are declared in a `PklProject` file must be _resolved_ via CLI command xref:pkl-cli:index.adoc#command-project-resolve[`pkl project resolve`].
This builds a single dependency list, resolving all transitive dependencies, and determines the appropriate version for each package.
It creates or updates a file called `PklProject.deps.json` in the project's root directory with the list of resolved dependencies.
When resolving version conflicts, the CLI will pick the latest link:{uri-semver}[semver] minor version of each package.
For example, if the project declares a dependency on package A at `1.2.0`, and a package transitively declares a dependency on package A at `1.3.0`, version `1.3.0` is selected.
In short, the algorithm has the following steps:
1. Gather a list of all dependencies, either directly declared or transitive.
2. For each dependency, keep only the newest minor version.
The resolve command is idempotent; given a PklProject file, it always produces the same set of resolved dependencies.
NOTE: This algorithm is adapted from Go's link:{uri-mvs-build-list}[minimum version selection].
==== Creating a Package
Projects enable the creation of a <<package-asset-uri,package>>.
To create a package, the `package` section of a `PklProject` module must be defined.
.PklProject
[source,pkl]
----
amends "pkl:Project"
package {
name = "mypackage" // <1>
baseUri = "package://example.com/\(name)" // <2>
version = "1.0.0" // <3>
packageZipUrl = "https://example.com/\(name)/\(name)@\(version).zip" // <4>
}
----
<1> The display name of the package.For display purposes only.
<2> The package URI, without the version part.
<3> The version of the package.
<4> The URL to download the package's ZIP file.
The package itself is created by the command xref:pkl-cli:index.adoc#command-project-package[`pkl project package`].
This command only prepares artifacts to be published.
Once the artifacts are prepared, they are expected to be uploaded to an HTTPS server such that the ZIP asset can be downloaded at path `packageZipUrl`, and the metadata can be downloaded at `+https://<package uri>+`.
[[local-dependencies]]
==== Local dependencies
A project can depend on a local project as a dependency.
This can be useful for:
* Structuring a monorepo that publishes multiple packages.
* Temporarily testing out library changes when used within another project.
To specify a local dependency, import its `PklProject` file.
The imported `PklProject` _must_ have a package section defined.
.birds/PklProject
[source,{pkl}]
----
amends "pkl:Project"
dependencies {
["fruit"] = import("../fruit/PklProject") // <1>
}
package {
name = "birds"
baseUri = "package://example.com/birds"
version = "1.8.3"
packageZipUrl = "https://example.com/birds@\(version).zip"
}
----
<1> Specify relative project `../fruit` as a dependency.
.fruit/PklProject
[source,{pkl}]
----
amends "pkl:Project"
package {
name = "fruit"
baseUri = "package://example.com/fruit"
version = "1.5.0"
packageZipUrl = "https://example.com/fruit@\(version).zip"
}
----
From the perspective of project `birds`, `fruit` is just another package.
It can be imported using dependency notation, i.e. `import "@fruit/Pear.pkl"`.
At runtime, it will resolve to relative path `../fruit/Pear.pkl`.
When packaging projects with local dependencies, both the project and its dependent project must be passed to the xref:pkl-cli:index.adoc#command-project-package[`pkl project package`] command.
[[external-readers]]
=== External Readers
External readers are a mechanism to extend the <<modules,module>> and <<resources,resource>> URI schemes that Pkl supports.
Readers are implemented as ordinary executables and use Pkl's xref:bindings-specification:message-passing-api.adoc[message passing API] to communicate with the hosting Pkl evaluator.
The xref:swift:ROOT:index.adoc[Swift] and xref:go:ROOT:index.adoc[Go] language binding libraries provide an `ExternalReaderRuntime` type to facilitate implementing external readers.
External readers are configured separately for modules and resources.
They are registered by mapping their URI scheme to the executable to run and additional arguments to pass.
This is done on the command line by passing `--external-resource-reader` and `--external-module-reader` flags, which may both be passed multiple times.
[source,text]
----
$ pkl eval <module> --external-resource-reader <scheme>=<executable> --external-module-reader <scheme>='<executable> <argument> <argument>'
----
External readers may also be configured in a <<projects, Project's>> `PklProject` file.
[source,{pkl}]
----
evaluatorSettings {
externalResourceReaders {
["<scheme>"] {
executable = "<executable>"
}
}
externalModuleReaders {
["<scheme>"] {
executable = "<executable>"
arguments { "<arg>"; "<arg>" }
}
}
}
----
Registering an external reader for a scheme automatically adds that scheme to the default allowed modules/resources.
As with Pkl's built-in module and resource schemes, setting explicit allowed modules or resources overrides this behavior and appropriate patterns must be specified to allow use of external readers.
==== Example
Consider this module:
[source,{pkl}]
----
username = "pigeon"
email = read("ldap://ds.example.com:389/dc=example,dc=com?mail?sub?(uid=\(username))").text
----
Pkl doesn't implement the `ldap:` resource URI scheme natively, but an external reader can provide it.
Assuming a hypothetical `pkl-ldap` executable implementing the external reader protocol and the `ldap:` scheme is in the `$PATH`, this module can be evaluated as:
[source,text]
----
$ pkl eval <module> --external-resource-reader ldap=pkl-ldap
username = "pigeon"
email = "pigeon@example.com"
----
In this example, the external reader may provide both `ldap:` and `ldaps:` schemes.
To support both schemes during evaluation, both would need to be registered explicitly:
[source,text]
----
$ pkl eval <module> --external-resource-reader ldap=pkl-ldap --external-resource-reader ldaps=pkl-ldap
----
[[mirroring_packages]]
=== Mirroring packages
A package is a shareable archive of modules and resources that are published to the internet.
A package's URI tells two things:
1. The name of the package.
2. Where the package is downloaded from.
For example, given the package name `package://example.com/mypackage@1.0.0`, Pkl will make an HTTPS request to `\https://example.com/mypackage@1.0.0` to fetch package metadata.
In situations where internet access is restricted, a mirror can be set up to allow use of packages that are published to the internet.
To direct Pkl to a mirror, the `--http-rewrite` CLI option (and its equivalent options when using Pkl's other evaluator APIs) must be used.
For example, `--http-rewrite \https://pkg.pkl-lang.org/=\https://my.internal.mirror/` will tell Pkl to download packages from host `my.internal.mirror`.
NOTE: To effectively mirror packages from pkg.pkl-lang.org, there must be two rewrites; one for `\https://pkg.pkl-lang.org/` (where package metadata is downloaded), and one for `\https://github.com/` (where package zip files are downloaded).
NOTE: Pkl does not provide any tooling to run a mirror server.
To fully set up mirroring, an HTTP(s) server will need be running, and which mirrors the same assets byte-for-byte.