Writing a Template
In parts one and two, you saw that Pkl provides validation of our configurations.
It checks syntax, types and constraints.
As you saw in the AcmeCICD
example here, the template can provide informative error messages when an amending module violates a type constraint.
In this final part, you will see some of Pkl’s techniques that are particularly relevant for writing a template.
Basic types
Pkl always checks the syntax of its input. As it evaluates your configuration, it also checks types. You’ve seen objects, listings, and mappings already. These provide ways to write structured configuration. Before you can write types for them, you need to know how to write the types for the simplest (unstructured) values.
These are all Pkl’s basic types:
name: String = "Writing a Template"
part: Int = 3
hasExercises: Boolean = true
amountLearned: Float = 13.37
duration: Duration = 30.min
bandwidthRequirementPerSecond: DataSize = 52.4288.mb
In the above, you’ve explicitly annotated the code with type signatures.
The default output of Pkl is actually pcf
, which is a subset of Pkl.
Since pcf
does not have type signatures, running Pkl on this example removes them.
$ pkl eval pklTutorialPart3.pkl
name = "Writing a Template"
part = 3
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 52.4288.mb
Note how Duration
and DataSize
help you prevent unit errors in these common (for configuration) domains.
Typed objects, properties and amending
Having a notation for basic types, you can now write typed objects.
class Language { (1)
name: String
}
bestForConfig: Language = new { (2)
name = "Pkl"
}
1 | A class definition. |
2 | A property definition, using the Language class. |
Although not required (or enforced), it’s customary to name properties starting with a lower-case letter. Class names, by that same convention, start with an upper-case letter. |
You can type objects with classes.
In this example, you define a class called Language
.
You can now be certain that every instance of Language
has a property name
with type String
.
Types and values are different things in Pkl. Pkl does not render types in its output,[1] so when you run Pkl on this, you don’t see the class definition at all.
$ pkl eval simpleClass.pkl
bestForConfig {
name = "Pkl"
}
Did you notice that the output doesn’t just omit the type signature, but also the = new
?
We will discuss this further in the next section.
When your configuration describes a few different parts like this, you can define one instance and amend it for every other instance.
For example:
class TutorialPart {
name: String
part: Int
hasExercises: Boolean
amountLearned: Float
duration: Duration
bandwidthRequirementPerSecond: DataSize
}
pklTutorialPart1: TutorialPart = new {
name = "Basic Configuration"
part = 1
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 50.mib.toUnit("mb")
}
pklTutorialPart2: TutorialPart = (pklTutorialPart1) {
name = "Filling out a Template"
part = 2
}
pklTutorialPart3: TutorialPart = (pklTutorialPart1) {
name = "Writing a Template"
part = 3
}
You can read this as saying "pklTutorialPart2
& pklTutorialPart3
are exactly like pklTutorialPart1
, except for their name
and part
."
Running Pkl confirms this:
$ pkl eval pklTutorialParts.pkl
pklTutorialPart1 {
name = "Basic Configuration"
part = 1
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 52.4288.mb
}
pklTutorialPart2 {
name = "Filling out a Template"
part = 2
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 52.4288.mb
}
pklTutorialPart3 {
name = "Writing a Template"
part = 3
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 52.4288.mb
}
Sadly, pklTutorialParts.pkl
is a rewrite of pklTutorialPart3.pkl
.
It creates a separate class TutorialPart
and instantiates three properties with it (pklTutorialPart1
, pklTutorialPart2
and pklTutorialPart3
).
In doing so, it implicitly moves everything "down" one level (pklTutorialPart3
is now a property in the module pklTutorialParts
, whereas above, in pklTutorialPart3.pkl
it was its own module).
This is not very DRY.
As a matter of fact, you don’t need this rewrite.
Any .pkl
file defines a module in Pkl.
Any module is represented by a module class, which is an actual Pkl class
.
A module is not quite the same as any other class, because Pkl never renders class definitions on the output.
However, when you ran Pkl on pklTutorialPart3.pkl
, it did produce an output.
This is because a module also defines an instance of the module class.
The values given to properties in a module (or in any "normal" class) are called default values. When you instantiate a class, all the properties for which you don’t provide a value are populated from the class' default values.
In our examples of tutorial parts, only the name
and part
varied across instances.
You can express this by adding default values to the (module) class definition.
Instead of starting from a particular tutorial part, you can define the module tutorialPart
as follows:
name: String (1)
part: Int (1)
hasExercises: Boolean = true (2)
amountLearned: Float = 13.37 (2)
duration: Duration = 30.min (2)
bandwidthRequirementPerSecond: DataSize = 52.4288.mb (2)
1 | No default value given. |
2 | Default value given. |
Running this through Pkl gives an error, or course, because of the missing values:
$ pkl eval TutorialPart.pkl
–– Pkl Error ––
Tried to read property `name` but its value is undefined.
1 | name: String
^^^^
...
An individual part now only has to fill in the missing fields, so you can change pklTutorialPart3.pkl
to amend this:
amends "TutorialPart.pkl"
name = "Writing a Template"
part = 3
This results in
$ pkl eval pklTutorialPart3.pkl
name = "Writing a Template"
part = 3
hasExercises = true
amountLearned = 13.37
duration = 30.min
bandwidthRequirementPerSecond = 52.4288.mb
This now behaves exactly like our pklTutorialPart3: TutorialPart = (pklTutorialPart1) {…
before.
pklTutorialPart3
is now defined as the value we get by amending tutorialPart
and giving it a name
and a part
.
Amending anything never changes its type.
When we amend an object of type |
A new template
Now that you know about types, you can start writing your first template.
So far, you’ve written configurations with Pkl, either without a template, or using the AcmeCICD
template from Amending templates.
It is often easiest to first write a (typical) configuration for which you want to create a template.
Suppose you want to define what a live workshop for this tutorial looks like.
Consider this example:
title = "Pkl: Configure your Systems in New Ways"
interactive = true
seats = 100
occupancy = 0.85
duration = 1.5.h
`abstract` = """
With more systems to configure, the software industry is drowning in repetitive and brittle configuration files.
YAML and other configuration formats have been turned into programming languages against their will.
Unsurprisingly, they don’t live up to the task.
Pkl puts you back in control.
"""
event {
name = "Migrating Birds between hemispheres"
year = 2024
}
instructors {
"Kate Sparrow"
"Jerome Owl"
}
sessions {
new {
date = "2/1/2024"
time = 30.min
}
new {
date = "2/1/2024"
time = 30.min
}
}
assistants {
["kevin"] = "Kevin Parrot"
["betty"] = "Betty Harrier"
}
agenda {
["beginners"] {
name = "Basic Configuration"
part = 1
duration = 45.min
}
["intermediates"] {
name = "Filling out a Template"
part = 2
duration = 45.min
}
["experts"] {
name = "Writing a Template"
part = 3
duration = 45.min
}
}
Call your new template Workshop.pkl
.
Although not required, it’s good practice to always name your template with a module
-clause.
Defining the first few properties are like you saw in the previous section:
module Workshop
title: String
interactive: Boolean
seats: Int
occupancy: Float
duration: Duration
`abstract`: String
Unlike these first few properties, event
is an object with multiple properties.
To be able to type event
, you need a class
.
You’ve seen before how to define this:
class Event {
name: String
year: Int
}
event: Event
Next, instructors
isn’t an object with properties, but a list of unnamed values.
Pkl offers the Listing
type for this:
instructors: Listing<String>
sessions
is a Listing
of objects, so you need a Session
class.
class Session {
time: Duration
date: String
}
sessions: Listing<Session>
assistants
has a structure like an object, in that all the values are named, but the set of names is not fixed for all possible workshops (and some workshops may have more assistants than others). The Pkl type for this is a Mapping
:
assistants: Mapping<String, String>
Finally, for every workshop session, there is an agenda
, which describes which TutorialPart
s are covered.
You already defined TutorialPart.pkl
as its own module, so you should not define a separate class, but rather import
that module and reuse it here:
import "TutorialPart.pkl" (1)
agenda: Mapping<String, TutorialPart>
1 | This import clause brings the name TutorialPart into scope, which is the module class as discussed above. Note that import clauses must appear before property definitions. |
Putting it all together, your Workshop.pkl
template looks like this:
module Workshop
import "TutorialPart.pkl"
title: String
interactive: Boolean
seats: Int
occupancy: Float
duration: Duration
`abstract`: String
class Event {
name: String
year: Int
}
event: Event
instructors: Listing<String>
class Session {
time: Duration
date: String
}
sessions: Listing<Session>
assistants: Mapping<String, String>
agenda: Mapping<String, TutorialPart>
class
and typealias
) themselves are never rendered.