Basic Configuration

In this first part of the Pkl tutorial, you build familiarity with Pkl syntax and basic structure. You also learn different ways to invoke Pkl to produce different formats.

Basic values

Consider the following example Pkl file.

intro.pkl
name = "Pkl: Configure your Systems in New Ways"
attendants = 100
isInteractive = true
amountLearned = 13.37

Running Pkl on this file gives

$ pkl eval /Users/me/tutorial/intro.pkl
name = "Pkl: Configure your Systems in New Ways"
attendants = 100
isInteractive = true
amountLearned = 13.37

It may seem nothing happened. However, Pkl tells you that it accepts the input. In other words, you now know that intro.pkl does not contain any errors.

You can ask Pkl to print this configuration in a different format, using the -f option. For example, JSON:

$ pkl eval -f json /Users/me/tutorial/intro.pkl
{
  "name": "Pkl: Configure your Systems in New Ways",
  "attendants": 100,
  "isInteractive": true,
  "amountLearned": 13.37
}

Or PropertyList format:

$ pkl eval -f plist /Users/me/tutorial/intro.pkl
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>name</key>
  <string>Pkl: Configure your Systems in New Ways</string>
  <key>attendants</key>
  <integer>100</integer>
  <key>isInteractive</key>
  <true/>
  <key>amountLearned</key>
  <real>13.37</real>
</dict>
</plist>

Notice that Pkl generated <string>, <integer>, <true/> and <real> for the values in your configuration. This means it has both correctly derived the types of the literal values and translated those types to the corresponding elements in the PropertyList. Part III goes into types in more detail.

Structure: Classes, objects, modules

A configuration often requires more than just basic values. Typically, you need some kind of (hierarchical) structure. Pkl provides immutable objects for this.

Objects have three kinds of members: properties, elements and entries. First, look at the syntax for objects and their members.

Properties

simpleObjectWithProperties.pkl
bird { (1)
  name = "Common wood pigeon" (2)
  diet = "Seeds"
  taxonomy { (3)
    species = "Columba palumbus"
  }
}
1 This defines bird to be an object
2 For primitive values, Pkl has the = syntax (more on this later).
3 Just like bird {, but to show that objects can be nested.

This defines an object called bird with three named properties: name, diet, and taxonomy. The first two of these are strings, but taxonomy is another object. This means properties in an object can have different types and objects can be nested.

Elements

Of course, you don’t always have names for every individual structure in your configuration. What if you want "just a bunch of things" without knowing how many? Pkl offers elements for this purpose. Elements are object members, just like properties. Where you index properties by their name, you index elements by an integer. You can think of an object that only contains elements as array. Much like arrays in many languages, you can use square brackets to access an element, for example, myObject[42].

You write an element, by writing only an expression. Pkl derives the index from the number of elements already in the object. For example:

simpleObjectsWithElements.pkl
exampleObjectWithJustIntElements {
  100 (1)
  42
}

exampleObjectWithMixedElements {
  "Bird Breeder Conference"
  (2000 + 23) (2)
  exampleObjectWithJustIntElements (3)
}
1 When you write only the value (without a name), you describe an element.
2 Elements don’t have to be literal values; they can be arbitrary expressions.
3 Elements can really be any value, not just primitive values.

Entries

Objects can have one more kind of member; entries. Like a property, an entry is "named" (technically keyed). Unlike a property, the name does not need to be known at declaration time. Of course, we need a syntax to tell entries apart from properties. You write entry "names" by enclosing them in square brackets ("names" is quoted, because the names do not need to be strings; any value can index entries).

simpleObjectsWithEntries.pkl
pigeonShelter {
  ["bird"] { (1)
    name = "Common wood pigeon"
    diet = "Seeds"
    taxonomy {
      species = "Columba palumbus"
    }
  }
  ["address"] = "355 Bird St." (2)
}

birdCount {
  [pigeonShelter] = 42 (3)
}
1 The difference with properties is the notation of the key: [<expression>].
2 As with properties, entries can be primitive values or objects.
3 Any object can be used as a key for an entry.

Mixed members

In the examples so far, you have seen objects with properties, object with elements and object with entries. These object members can be freely mixed.

mixedObject.pkl
mixedObject {
  name = "Pigeon"
  lifespan = 8
  "wing"
  "claw"
  ["wing"] = "Not related to the _element_ \"wing\""
  42
  extinct = false
  [false] {
    description = "Construed object example"
  }
}

Notice, how properties (name, lifespan and extinct), elements ("wing", "claw", 42) and entries ("wing", false) are mixed together in this one object. You don’t have to order them by kind, and you don’t require (other) special syntax.

Collections

This free-for-all mixing of object members can become confusing. Also, target formats are often considerably more restrictive. In the following example, you see what happens when you try to produce JSON from mixedObject:

$ pkl eval -f json /Users/me/tutorial/mixedObject.pkl
–– Pkl Error ––
Cannot render object with both properties/entries and elements as JSON.
Object: "Pigeon"

89 | text = renderer.renderDocument(value)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.24.0/stdlib/base.pkl#L90)

This is why Pkl has two special types of object, namely listings, which contain exclusively elements, and mappings, which contain exclusively entries. Both listings and mappings are "just objects," so, they don’t require syntax besides that of objects:

collections.pkl
birds { (1)
  "Pigeon"
  "Parrot"
  "Barn owl"
  "Falcon"
}

habitats { (2)
  ["Pigeon"] = "Streets"
  ["Parrot"] = "Parks"
  ["Barn owl"] = "Forests"
  ["Falcon"] = "Mountains"
}
1 A listing containing four elements.
2 A mapping containing four entries.

Technically, the correct way to define birds and habitats is by using new Listing {…​} and new Mapping {…​} explicitly. You will see what these mean in part three of this tutorial.

When you render this configuration as JSON, everything works:

{
  "birds": [
    "Pigeon",
    "Parrot",
    "Barn owl",
    "Falcon"
  ],
  "habitats": {
    "Pigeon": "Streets",
    "Parrot": "Parks",
    "Barn owl": "Forests",
    "Falcon": "Mountains"
  }
}

Notice particularly, that you rendered the listing as a JSON array. When you index the listing with an integer, you’re referring to the element inside the listing at the corresponding position (starting from 0). For example:

indexedListing.pkl
birds {
  "Pigeon"
  "Parrot"
  "Barn owl"
  "Falcon"
}

relatedToSnowOwl = birds[2]

results in

birds {
  "Pigeon"
  "Parrot"
  "Barn owl"
  "Falcon"
}
relatedToSnowOwl = "Barn owl"

Exercises

  1. Given the following JSON snippet (taken from W3C examples), write the .pkl file that produces this JSON:

    {
      "name": "Common wood pigeon",
      "lifespan": 8,
      "friends": {
        "bird1": "Parrot",
        "bird2": "Albatross",
        "bird3": "Falcon"
      }
    }
  2. For some reason, we decide we no longer need the birdX names of the different birds; we just need them as an array. Change your solution to the previous question to produce the following JSON result:

    {
      "name": "Common wood pigeon",
      "lifespan": 8,
      "birds": ["Parrot", "Barn owl", "Falcon"]
    }