mirror of
https://github.com/apple/pkl.git
synced 2026-01-18 09:27:32 +01:00
442 lines
11 KiB
Plaintext
442 lines
11 KiB
Plaintext
= Writing a Template
|
||
include::ROOT:partial$component-attributes.adoc[]
|
||
|
||
In parts xref:01_basic_config.adoc[one] and xref:02_filling_out_a_template.adoc[two], you saw that Pkl provides _validation_ of our configurations.
|
||
It checks syntax, types and constraints.
|
||
As you saw in the `AcmeCICD` example xref:02_filling_out_a_template.adoc#amending-templates[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:
|
||
|
||
[source,{pkl}]
|
||
.pklTutorialPart3.pkl
|
||
----
|
||
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.
|
||
|
||
[source,shell]
|
||
----
|
||
$ 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 https://en.wikipedia.org/wiki/Mars_Climate_Orbiter[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_.
|
||
|
||
[source,{pkl}]
|
||
.simpleClass.pkl
|
||
----
|
||
class Language { // <1>
|
||
name: String
|
||
}
|
||
|
||
bestForConfig: Language = new { // <2>
|
||
name = "Pkl"
|
||
}
|
||
----
|
||
<1> A class definition.
|
||
<2> A property definition, using the `Language` class.
|
||
|
||
[NOTE]
|
||
====
|
||
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,footnote:[Although, some output formats can contain their own form of type annotation. This may be derived from the Pkl type. Type definitions (`class` and `typealias`) themselves are never rendered.] so when you run Pkl on this, you don't see the class _definition_ at all.
|
||
|
||
[source,{pkl}]
|
||
----
|
||
$ 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:
|
||
|
||
[source,{pkl}]
|
||
.pklTutorialParts.pkl
|
||
----
|
||
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:
|
||
|
||
[source,shell]
|
||
----
|
||
$ 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:
|
||
|
||
[source,{pkl}]
|
||
.TutorialPart.pkl
|
||
----
|
||
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:
|
||
|
||
[source, shell]
|
||
----
|
||
$ 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:
|
||
|
||
[source,{pkl}]
|
||
.pklTutorialPart3.pkl
|
||
----
|
||
amends "TutorialPart.pkl"
|
||
|
||
name = "Writing a Template"
|
||
|
||
part = 3
|
||
----
|
||
|
||
This results in
|
||
|
||
[source, shell]
|
||
----
|
||
$ 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`.
|
||
|
||
[IMPORTANT]
|
||
====
|
||
Amending anything _never changes its type_.
|
||
When we amend an object of type `Foo`, the result will always be precisely of type `Foo`.
|
||
By "precisely" we mean, that amending an object also can't "turn it into" an instance of a sub-class of the class of the object being amended.
|
||
====
|
||
|
||
== 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 xref:02_filling_out_a_template.adoc#amending-templates[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:
|
||
|
||
[source,{pkl}]
|
||
.workshop2024.pkl
|
||
----
|
||
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:
|
||
|
||
[source,{pkl}]
|
||
----
|
||
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:
|
||
|
||
[source,{pkl}]
|
||
----
|
||
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:
|
||
|
||
|
||
[source,{pkl}]
|
||
----
|
||
instructors: Listing<String>
|
||
----
|
||
|
||
`sessions` is a `Listing` of objects, so you need a `Session` class.
|
||
[source,{pkl}]
|
||
----
|
||
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`:
|
||
|
||
[source,{pkl}]
|
||
----
|
||
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:
|
||
|
||
[source,{pkl}]
|
||
----
|
||
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:
|
||
|
||
[source,{pkl}]
|
||
.Workshop.pkl
|
||
----
|
||
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>
|
||
----
|