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). |
|
camelCase |
It is designed to be used as a value. |
|
kebab-case |
It is designed to be used as a CLI tool. |
|
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
2.1. Header
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
orextends
clause -
Import clauses
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.
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.
/// 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:
-
Properties
-
Methods
-
Classes and type aliases
-
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.
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 ../
.
amends ".../ancestor.pkl"
import ".../ancestor2.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.
foo = "bar"
baz = "buz"
foo = "bar"
baz = "buz"
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.
/// Denotes something.
myFoo: String
/// Something else
myOtherFoo: String
/// 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.
myFoo: Foo = new { foo = "bar" }
open class Foo {}
class Bar extends Foo {}
foo: Foo = new Bar {}
This is okay because this is meaning to initialize Bar
instead of Foo
.
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 //
.
/// 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.
/// 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 */
.
/* Let's have a zebra party */
/*Let's have a zebra party*/
5. Classes
5.1. Class names
Name classes in PascalCase.
class ZebraParty {}
class zebraParty {}
class zebraparty {}
6. Strings
6.1. Custom String Delimiters
Use custom string delimiters to avoid the need for string escaping.
myString = #"foo \ bar \ baz"#
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.
greeting = "Hello, \(name)"
greeting = "Hello, " + name
7. Formatting
7.1. Line width
Lines shouldn’t exceed 100 characters.
Exceptions:
-
String literals
-
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.
foo =
"foo"
bar =
new {
baz = "baz"
biz = "biz"
}
foo =
"foo"
bar =
new {
baz = "baz"
biz = "biz"
}
An assignee that starts on the same line should not be indented.
foo = new {
baz = "baz"
biz = "biz"
}
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
.
if (bar)
bar
else
foo
if (bar) bar else foo
if (bar) bar
else foo
let (foo = "bar")
foo.toUpperCase()
let (foo = "bar") foo.toUpperCase()
if (bar)
bar
else
foo
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.
if (bar)
bar
else if (baz)
baz
else
foo
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.
foo = bar
|> baz
|> biz
myNum = 1
+ 2
+ 3
+ 4
foo = bar |>
baz |>
biz
myNum = 1 +
2 +
3 +
4
foo = bar
|> baz
|> biz
foo = bar
|> baz
|> biz
Exception: the minus operator must come before the newline, because otherwise it is parsed as a unary minus.
myNum = 1 -
2 -
3 -
4
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.
res1 = new { bar = "bar"; baz = "baz" }
res2 = new { 1; 2; 3; 4; 5; 6 }
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.
res {
foo = "foo"
bar = "bar"
}
res2 {
["foo"] = "foo"
["bar"] = "bar"
}
res3 {
"foo"
"bar"
}
res {
foo = "foo"
bar = "bar"
}
res2 {
["foo"] = "foo"
["bar"] = "bar"
}
res3 {
"foo"
"bar"
}
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.
res {
foo = "foo"
bar = "bar"
}
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.
numbers {
1
2
3
4
}
squares {
for (num in numbers) {
num ** 2
}
}
numbers {
1
2
3
4
}
squares = numbers.toList().map((num) -> num ** 2).toListing()