Pkl Style Guide

This document serves as the Pkl team’s recommended coding standard for the Pkl configuration language.

1. Files

1.1. Filename

Use the .pkl extension for all files.

Follow these rules for casing the file’s name:

Casing

Description

Example

PascalCase

It is designed to be used as a template, or used as a class (i.e. imported and instantiated).

K8sResource.pkl

camelCase

It is designed to be used as a value.

myDeployment.pkl

kebab-case

It is designed to be used as a CLI tool.

do-convert.pkl

Exception: If a file is meant to render into a static configuration file, the filename should match the target file’s name without the extension. For example, config.pkl turns into config.yml.

Exception: The PklProject file cannot have any extension.

1.2. File Encoding

Encode all files using UTF-8.

2. Module Structure

Separate each section of the module header by one blank line.

A module header consists of the following clauses, each of which is optional:

  • Module clause

  • amends or extends clause

  • Import clauses

module.pkl
module com.example.Foo (1)

extends "Bar.pkl" (2)

import "baz.pkl" (3)
import "Buz.pkl" (3)
1 Module clause
2 extends clause
3 Import clause

2.1.1. Module name

Match the name of the module with the name of the file.

MyModule.pkl
module MyModule

If a module is meant to be published, add a module clause, @ModuleInfo annotation, and doc comments.

Modules that do not get published anywhere may omit a module clause.

MyModule.pkl
/// Used for some type of purpose. (1)
@ModuleInfo { minPklVersion = "0.24.0" } (2)
module MyModule (3)
1 Doc comments
2 @ModuleInfo annotation
3 Module clause

2.1.2. amends vs. extends clause

A module that doesn’t add new properties shouldn’t use the extends clause.

2.1.3. Imports

Sort imports sections using natural sorting by their module URI. Relative path imports should be in their own section, separated by a newline. There should be no unused imports.

import "modulepath:/foo.pkl"
import "package://example.com/mypackage@1.0.0#/foo.pkl"

import ".../my/file/bar2.pkl"
import ".../my/file/bar11.pkl"

2.2. Module body

Within a module body, define members in this order:

  1. Properties

  2. Methods

  3. Classes and type aliases

  4. The amended output property.

Exception: local members can be close to their usage.

Exception: functions meant to be a class constructor can be next to the class declaration.

constructor.pkl
function MyClass(_name: String): MyClass = new { name = _name }

class MyClass {
  name: String
}

2.3. Module URIs

If possible, use triple-dot Module URIs to reference ancestor modules instead of multiple ../.

good.pkl
amends ".../ancestor.pkl"

import ".../ancestor2.pkl"
bad.pkl
amends "../../../ancestor.pkl"

import "../../../ancestor2.pkl"

3. Objects

3.1. Member spacing

Object members (properties, elements, and entries) should be separated by at most one blank line.

good.pkl
foo = "bar"

baz = "buz"
good.pkl
foo = "bar"
baz = "buz"
bad.pkl
foo = "bar"


baz = "buz"

Too many lines separate foo and baz.

3.2. Overridden properties

Properties that override an existing property shouldn’t have doc comments nor type annotations, unless the type is intentionally overridden via extends.

amends "myOtherModule.pkl"

foo = "bar"

3.3. New property definitions

Each property definition should have a type annotation and doc comment. Successive definitions should be separated by a blank line.

good.pkl
/// Denotes something.
myFoo: String

/// Something else
myOtherFoo: String
bad.pkl
/// Denotes something.
myFoo: String
/// Something else
myOtherFoo: String

3.4. Objects with new

When initializing a Typed object using new, omit the type. For example, use new {} instead of new Foo {}.

This rule does not apply when initializing a property to a subtype of the property’s declared type.

good.pkl
myFoo: Foo = new { foo = "bar" }
good.pkl
open class Foo {}
class Bar extends Foo {}

foo: Foo = new Bar {}

This is okay because this is meaning to initialize Bar instead of Foo.

bad.pkl
myFoo1: Foo = new Foo { foo = "bar" } (1)

myFoo2 = new Foo { foo = "bar" } (2)
1 Unnecessary new Foo { …​ }
2 Unless amending/extending a module where myFoo2 is already defined, myFoo2 is effectively the unknown type, i.e. myFoo2: unknown.

4. Comments

Use doc comments to convey information to users of a module. Use line comments or block comments to convey implementation concerns to authors of a module, or to comment out code.

4.1. Doc comments

Doc comments should start with a one sentence summary paragraph, followed by additional paragraphs if necessary. Start new sentences on their own line. Add a single space after ///.

/// The time allotted for eating lunch.
///
/// Note:
/// * Hamburgers typically take longer to eat than salad.
/// * Pizza gets prepared per-order.
///
/// Orders must be placed on-prem.
/// See <https://cafeteria.com> for more details.
lunchHours: Duration

4.2. Line comments

If a comment relates to a property definition, place it after the property’s doc comments. Add a single space after //.

good.pkl
/// Designates whether it is zebra party time.
// TODO: Add constraints here?
partyTime: Boolean

A line comment may also be placed at the end of a line, as long as the line doesn’t exceed 100 characters.

good.pkl
/// Designates whether it is zebra party time.
partyTime: Boolean // TODO: Add constraints here?

4.3. Block comments

A single-line block comment should have a single space after /* and before */.

good.pkl
/* Let's have a zebra party */
bad.pkl
/*Let's have a zebra party*/

5. Classes

5.1. Class names

Name classes in PascalCase.

good.pkl
class ZebraParty {}
bad.pkl
class zebraParty {}
class zebraparty {}

6. Strings

6.1. Custom String Delimiters

Use custom string delimiters to avoid the need for string escaping.

good.pkl
myString = #"foo \ bar \ baz"#
bad.pkl
myString = "foo \\ bar \\ baz"
Sometimes, using custom string delimiters makes source code harder to read. For example, the \# literal reads better using escapes ("\\#") than using custom string delimimters (##"\#"##).

6.2. Interpolation

Prefer interpolation to string concatenation.

good.pkl
greeting = "Hello, \(name)"
bad.pkl
greeting = "Hello, " + name

7. Formatting

7.1. Line width

Lines shouldn’t exceed 100 characters.

Exceptions:

  1. String literals

  2. Code snippets within doc comments

7.2. Indentation

Use two spaces per indentation level.

7.2.1. Members within braces

Members within braces should be indented one level deeper than their parents.

foo {
  bar {
    baz = "hi"
  }
}

7.2.2. Assignment operator (=)

An assignee that starts after a newline should be indented.

good.pkl
foo =
  "foo"

bar =
  new {
    baz = "baz"
    biz = "biz"
  }
bad.pkl
foo =
"foo"

bar =
new {
  baz = "baz"
  biz = "biz"
}

An assignee that starts on the same line should not be indented.

good.pkl
foo = new {
  baz = "baz"
  biz = "biz"
}
bad.pkl
foo = new {
    baz = "baz"
    biz = "biz"
  }

7.2.3. if and let expressions

if and let bodies that start on their own line should be indented. Child bodies may also be inline, and the else branch of if expressions may be inline of if.

good.pkl
if (bar)
  bar
else
  foo
good.pkl
if (bar) bar else foo
good.pkl
if (bar) bar
else foo
good.pkl
let (foo = "bar")
  foo.toUpperCase()
good.pkl
let (foo = "bar") foo.toUpperCase()
bad.pkl
if (bar)
bar
else
foo
bad.pkl
let (foo = "bar")
foo.toUpperCase()

Exception: A nested if expression within the else branch should have the same indentation level as its parent, and start on the same line as the parent else keyword.

good.pkl
if (bar)
  bar
else if (baz)
  baz
else
  foo
bad.pkl
if (bar)
  bar
else
  if (baz)
    baz
  else
    foo

7.2.4. Multiline chained method calls

Indent successive multiline chained method calls.

foo()
  .bar()
  .baz()
  .biz()

7.2.5. Multiline binary operators

Place operators after the newline, and indent successive lines to the same level.

good.pkl
foo = bar
  |> baz
  |> biz

myNum = 1
  + 2
  + 3
  + 4
bad.pkl
foo = bar |>
  baz |>
  biz

myNum = 1 +
  2 +
  3 +
  4
bad.pkl
foo = bar
|> baz
|> biz
bad.pkl
foo = bar
  |> baz
    |> biz

Exception: the minus operator must come before the newline, because otherwise it is parsed as a unary minus.

good.pkl
myNum = 1 -
  2 -
  3 -
  4
bad.pkl
myNum = 1
  - 2
  - 3
  - 4

7.3. Spaces

Add a space:

amends "Foo.pkl" (1)

res1 { "foo" } (2)
res2 = 1 + 2 (3)
res3 = res2 as Number (3)
res4 = List(1, 2, 3) (4)
res5 = if (foo) bar else baz (5)
1 After keywords
2 Before and after braces
3 Around infix operators
4 After a comma
5 Before opening parentheses in control operators (if, for, when are control operators)
No spaces are added around the pipe symbol (|) in union types.
typealias Foo = "foo"|"bar"|"baz"

7.4. Object bodies

7.4.1. Single line

An object body may be a single line if it only consists of primitive elements, or if it contains two or fewer members. Otherwise, split them into multiple lines.

Separate each member of a single line object with a semicolon and a space.

good.pkl
res1 = new { bar = "bar"; baz = "baz" }
res2 = new { 1; 2; 3; 4; 5; 6 }
bad.pkl
res1 = new { bar = "bar"; baz = "baz"; biz = "biz"; } (1)

res2 = new { 1 2 3 4 5 6 } (2)
1 Too many members and trailing ;
2 No semicolon

7.4.2. Multiline

Multiline objects should have their members separated by at least one line break and at most one blank line.

good.pkl
res {
  foo = "foo"
  bar = "bar"
}

res2 {
  ["foo"] = "foo"
  ["bar"] = "bar"
}

res3 {
  "foo"
  "bar"
}
good.pkl
res {
  foo = "foo"

  bar = "bar"
}

res2 {
  ["foo"] = "foo"

  ["bar"] = "bar"
}

res3 {
  "foo"

  "bar"
}
bad.pkl
res {
  foo = "foo"


  bar = "bar" (1)
}

res2 {
  ["foo"] = "foo"


  ["bar"] = "bar" (1)
}

res3 {
  "foo"


  "bar" (1)
}

res4 {
  foo = "foo"; bar = "bar" (2)
}
1 Too many blank lines between members
2 No line break separating members

Put the opening brace on the same line.

good.pkl
res {
  foo = "foo"
  bar = "bar"
}
bad.pkl
res
{
  foo = "foo"
  bar = "bar"
}

8. Programming Practices

8.1. Prefer for generators

When programmatically creating elements and entries, prefer for generators over using the collection API. Using for generators preserves late binding.

good.pkl
numbers {
  1
  2
  3
  4
}

squares {
  for (num in numbers) {
    num ** 2
  }
}
bad.pkl
numbers {
  1
  2
  3
  4
}

squares = numbers.toList().map((num) -> num ** 2).toListing()