Files
pkl/docs/modules/language-tutorial/pages/03_writing_a_template.adoc
2024-05-13 10:09:43 -07:00

442 lines
11 KiB
Plaintext
Raw Permalink 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.
= 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 dont 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>
----