https://pkl-lang.org/blog/introducing-pkl.html
Pkl Pkl // Docs
[ ]
Language
Bindings
Java Kotlin Swift Go
Frameworks
Spring (Boot)
Editors
IntelliJ Neovim VS Code
Resources
GitHub Package Docs Style Guide Security
Community
GitHub Discussions Blog
Pkl Blog
*
+ Introducing Pkl, a programming language for configuration
Pkl Blog
* IntelliJ Plugin
+ 0.26.0
* Main Project
+ 0.26.0-dev
+ 0.25.1
* Neovim Plugin
+ 0.6.0
* Pkl Blog
* Pkl Go Bindings
+ 0.5.1
* Pkl Swift Bindings
+ 0.2.3
* ROOT
* Spring Boot Integration
+ 0.15.0
* VS Code Extension
+ 0.11.0
* Pkl Blog
* Introducing Pkl, a programming language for configuration
Edit this Page
Introducing Pkl, a programming language for configuration
by the Pkl Team on February 1st, 2024
We are delighted to announce the open source first release of Pkl
(pronounced Pickle), a programming language for producing
configuration.
When thinking about configuration, it is common to think of static
languages like JSON, YAML, or Property Lists. While these languages
have their own merits, they tend to fall short when configuration
grows in complexity. For example, their lack of expressivity means
that code often gets repeated. Additionally, it can be easy to make
configuration errors, because these formats do not provide any
validation of their own.
To address these shortcomings, sometimes formats get enhanced by
ancillary tools that add special logic. For example, perhaps there's
a need to make code more DRY, so a special property is introduced
that understands how to resolve references, and merge objects
together. Alternatively, there's a need to guard against validation
errors, so some new way is created to validate a configuration value
against an expected type. Before long, these formats almost become
programming languages, but ones that are hard to understand and hard
to write.
On the other end of the spectrum, a general-purpose language might be
used instead. Languages like Kotlin, Ruby, or JavaScript become the
basis for DSLs that generate configuration data. While these
languages are tremendously powerful, they can be awkward to use for
describing configuration, because they are not oriented around
defining and validating data. Additionally, these DSLs tend to be
tied to their own ecosystems. It is a hard sell to use a Kotlin DSL
as the configuration layer for an application written in Go.
We created Pkl because we think that configuration is best expressed
as a blend between a static language and a general-purpose
programming language. We want to take the best of both worlds; to
provide a language that is declarative and simple to read and write,
but enhanced with capabilities borrowed from general-purpose
languages. When writing Pkl, you are able to use the language
features you'd expect, like classes, functions, conditionals, and
loops. You can build abstraction layers, and share code by creating
packages and publishing them. Most importantly, you can use Pkl to
meet many different types of configuration needs. It can be used to
produce static configuration files in any format, or be embedded as a
library into another application runtime.
We designed Pkl with three overarching goals:
* To provide safety by catching validation errors before
deployment.
* To scale from simple to complex use-cases.
* To be a joy to write, with our best-in-class IDE integrations.
A Quick Tour of Pkl
We created Pkl to have a familiar syntax to developers, and to be
easy to learn. That is why we've included features like classes,
functions, loops, and type annotations.
For example, here is a Pkl file (module) that defines a configuration
schema for an imaginary web application.
This file defines types, and not data. This is a common pattern in
Pkl, and we call this a template.
Application.pkl
module Application
/// The hostname that this server responds to.
hostname: String
/// The port to listen on.
port: UInt16
/// The environment to deploy to.
environment: Environment
/// The database connection for this application
database: Database
class Database {
/// The username for this database.
username: String
/// The password for this database.
password: String
/// The remote host for this database.
host: String
/// The remote port for this database.
port: UInt16
/// The name of the database.
dbName: String
}
typealias Environment = "dev"|"qa"|"prod"
And here is how configuration data might be defined:
localhost.pkl
amends "Application.pkl"
hostname = "localhost"
port = 3599
environment = "dev"
database {
host = "localhost"
port = 5786
username = "admin"
password = read("env:DATABASE_PASSWORD") (1)
dbName = "myapp"
}
1 Built-in read expression for reading external resources.
It is easy to create variations of the same base data by amending.
For example, let's imagine that we want to run four databases
locally, as sidecars. This uses a for generator to produce four
variations, each of which amends the base db and specifies a
different port.
sidecars.pkl
import "Application.pkl"
hidden db: Application.Database = new {
host = "localhost"
username = "admin"
password = read("env:DATABASE_PASSWORD")
dbName = "myapp"
}
sidecars {
for (offset in List(0, 1, 2, 3)) {
(db) {
port = 6000 + offset
}
}
}
Pkl programs can be easily rendered to common formats.
* YAML
* JSON
* XML
$ export DATABASE_PASSWORD=hunter2
$ pkl eval --format yaml sidecars.pkl
sidecars:
- username: admin
password: hunter2
host: localhost
port: 6000
dbName: myapp
- username: admin
password: hunter2
host: localhost
port: 6001
dbName: myapp
- username: admin
password: hunter2
host: localhost
port: 6002
dbName: myapp
- username: admin
password: hunter2
host: localhost
port: 6003
dbName: myapp
$ export DATABASE_PASSWORD=hunter2
$ pkl eval --format json sidecars.pkl
{
"sidecars": [
{
"username": "admin",
"password": "hunter2",
"host": "localhost",
"port": 6000,
"dbName": "myapp"
},
{
"username": "admin",
"password": "hunter2",
"host": "localhost",
"port": 6001,
"dbName": "myapp"
},
{
"username": "admin",
"password": "hunter2",
"host": "localhost",
"port": 6002,
"dbName": "myapp"
},
{
"username": "admin",
"password": "hunter2",
"host": "localhost",
"port": 6003,
"dbName": "myapp"
}
]
}
$ export DATABASE_PASSWORD=hunter2
$ pkl eval --format xml sidecars.pkl
admin
hunter2
localhost
6000
myapp
admin
hunter2
localhost
6001
myapp
admin
hunter2
localhost
6002
myapp
admin
hunter2
localhost
6003
myapp
Built-in Validation
Configuration is about data. And data needs to be valid.
In Pkl, validation is achieved using type annotations. And, type
annotations can optionally have constraints defined on them.
Here is an example, that defines the following constraints:
* age must be between 0 and 130.
* name to not be empty.
* zipCode must be a string with five digits.
Person.pkl
module Person
name: String(!isEmpty)
age: Int(isBetween(0, 130))
zipCode: String(matches(Regex("\\d{5}")))
A failing constraint causes an evaluation error.
alessandra.pkl
amends "Person.pkl"
name = "Alessandra"
age = -5
zipCode = "90210"
Evaluating this module fails:
$ pkl eval alessandra.pkl
-- Pkl Error --
Type constraint `isBetween(0, 130)` violated.
Value: -5
5 | age: Int(isBetween(0, 130))
^^^^^^^^^^^^^^^^^
at Person#age (file:///Person.pkl)
5 | age = -5
^^
at alessandra#age (file:///alessandra.pkl)
106 | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.25.0/stdlib/base.pkl#L106)
Constraints are arbitrary expressions. This allows you to author
types that can express any type of check that can be expressed in
Pkl. Here is a sample type that must be a string with an odd length,
and whose first letter matches the last letter.
name: String(length.isOdd, chars.first == chars.last)
Sharing Packages
Pkl provides the ability to publish packages, and to import them as
dependencies in a project. This provides an easy way to share Pkl
code that can be used in other projects.
It is easy to create your own package and publish them as GitHub
releases, or to upload them anywhere you wish.
Packages can be imported via the absolute URI:
import "package://pkg.pkl-lang.org/pkl-pantry/pkl.toml@1.0.0#/toml.pkl"
output {
renderer = new toml.Renderer {}
}
Alternatively, they can be managed as dependencies of a project.
Using a project allows Pkl to resolve version conflicts between
different versions of the same dependency within a dependency graph.
It also means that you can import packages by a simpler name.
PklProject
amends "pkl:Project"
dependencies {
["toml"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/pkl.toml@1.0.0" }
}
myconfig.pkl
import "@toml/toml.pkl"
output {
renderer = new toml.Renderer {}
}
A set of packages are maintained by us, the Pkl team. These include:
* pkl-pantry -- a monorepo that publishes many different packages.
* pkl-k8s -- templates for defining Kubernetes descriptors.
Language Bindings
Pkl can produce configuration as textual output, and it can also be
embedded as a library into other languages via our language bindings.
When binding to a language, Pkl schema can be generated as classes/
structs in the target language. For example, the Application.pkl
example from above can be generated into Swift, Go, Java, and Kotlin.
Pkl even includes documentation comments in the target language.
* Swift
* Go
* Java
* Kotlin
import PklSwift
public enum Application {}
extension Application {
public enum Environment: String, CaseIterable, Decodable, Hashable {
case dev = "dev"
case qa = "qa"
case prod = "prod"
}
public struct Module: PklRegisteredType, Decodable, Hashable {
public static var registeredIdentifier: String = "Application"
/// The hostname that this server responds to.
public var hostname: String
/// The port to listen on.
public var port: UInt16
/// The environment to deploy to.
public var environment: Environment
/// The database connection for this application
public var database: Database
public init(hostname: String, port: UInt16, environment: Environment, database: Database) {
self.hostname = hostname
self.port = port
self.environment = environment
self.database = database
}
}
public struct Database: PklRegisteredType, Decodable, Hashable {
public static var registeredIdentifier: String = "Application#Database"
/// The username for this database.
public var username: String
/// The password for this database.
public var password: String
/// The remote host for this database.
public var host: String
/// The remote port for this database.
public var port: UInt16
/// The name of the database.
public var dbName: String
public init(username: String, password: String, host: String, port: UInt16, dbName: String) {
self.username = username
self.password = password
self.host = host
self.port = port
self.dbName = dbName
}
}
}
Application.pkl.go
package application
type Application struct {
// The hostname that this server responds to.
Hostname string `pkl:"hostname"`
// The port to listen on.
Port uint16 `pkl:"port"`
// The environment to deploy to.
Environment Environment.Environment `pkl:"environment"`
// The database connection for this application
Database *Database `pkl:"database"`
}
Database.pkl.go
// Code generated from Pkl module `Application`. DO NOT EDIT.
package application
type Database struct {
// The username for this database.
Username string `pkl:"username"`
// The password for this database.
Password string `pkl:"password"`
// The remote host for this database.
Host string `pkl:"host"`
// The remote port for this database.
Port uint16 `pkl:"port"`
// The name of the database.
DbName string `pkl:"dbName"`
}
environment/Environment.pkl.go
// Code generated from Pkl module `Application`. DO NOT EDIT.
package Environment
import (
"encoding"
"fmt"
)
type Environment string
const (
Dev Environment = "dev"
Qa Environment = "qa"
Prod Environment = "prod"
)
// String returns the string representation of Environment
func (rcv Environment) String() string {
return string(rcv)
}
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
import java.lang.StringBuilder;
import java.util.Objects;
import org.pkl.config.java.mapper.Named;
import org.pkl.config.java.mapper.NonNull;
public final class Application {
/**
* The hostname that this server responds to.
*/
public final @NonNull String hostname;
/**
* The port to listen on.
*/
public final int port;
/**
* The environment to deploy to.
*/
public final @NonNull Environment environment;
/**
* The database connection for this application
*/
public final @NonNull Database database;
public Application(@Named("hostname") @NonNull String hostname, @Named("port") int port,
@Named("environment") @NonNull Environment environment,
@Named("database") @NonNull Database database) {
this.hostname = hostname;
this.port = port;
this.environment = environment;
this.database = database;
}
public static final class Database {
/**
* The username for this database.
*/
public final @NonNull String username;
/**
* The password for this database.
*/
public final @NonNull String password;
/**
* The remote host for this database.
*/
public final @NonNull String host;
/**
* The remote port for this database.
*/
public final int port;
/**
* The name of the database.
*/
public final @NonNull String dbName;
public Database(@Named("username") @NonNull String username,
@Named("password") @NonNull String password, @Named("host") @NonNull String host,
@Named("port") long port, @Named("dbName") @NonNull String dbName) {
this.username = username;
this.password = password;
this.host = host;
this.port = port;
this.dbName = dbName;
}
}
public enum Environment {
DEV("dev"),
QA("qa"),
PROD("prod");
private String value;
private Environment(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
}
}
}
import kotlin.Int
import kotlin.Long
import kotlin.String
data class Application(
/**
* The hostname that this server responds to.
*/
val hostname: String,
/**
* The port to listen on.
*/
val port: Int,
/**
* The environment to deploy to.
*/
val environment: Environment,
/**
* The database connection for this application
*/
val database: Database
) {
data class Database(
/**
* The username for this database.
*/
val username: String,
/**
* The password for this database.
*/
val password: String,
/**
* The remote host for this database.
*/
val host: String,
/**
* The remote port for this database.
*/
val port: Int,
/**
* The name of the database.
*/
val dbName: String
)
enum class Environment(
val value: String
) {
DEV("dev"),
QA("qa"),
PROD("prod");
override fun toString() = value
}
}
Using code generation is just one of the many ways to embed Pkl
within an application. Our language bindings also provide evaluator
APIs to control Pkl evaluation at a low level, and users are free to
interact with Pkl at the abstraction level that makes the most sense
for their application.
Editor Support
We believe that a programming language is only as good as the
experience of writing it. That is why we aim to provide best-in-class
editor support. When writing Pkl in an editor, users are guided
through the process of filling in configuration data from a given
template. Additionally, the editors provide instant feedback if any
values are invalid, and documentation is immediately available when
called upon.
We are also releasing our IntelliJ plugin, which provides rich
support for JetBrains editors, including IntelliJ, Webstorm, GoLand,
and PyCharm. These plugins are able to analyze a Pkl program and
provide features like autocompletion, go-to-definition, and
refactoring support.
Here are some of the features that are available:
* Autocompletion
* Navigation
* Validation
Autocomplete in IntelliJ
Navigation in IntelliJ
Validation in IntelliJ
In addition, we are also planning on supporting the Language Server
Protocol, which will provide a similar level of integration in other
editors.
We are also releasing two other plugins: our VS Code plugin, and our
neovim plugin. Today, these plugins only provide basic editing
features like syntax highlighting and code folding.
Next Steps
We hope you like what we've shown you so far. For a more in-depth
guide, take a look at our tutorial. To learn more about the language
itself, read through our language reference. To connect with us, feel
free to submit a topic on GitHub Discussions.
Additionally, feel free to browse our sample repositories to get an
idea for what it's like to use Pkl:
* https://github.com/apple/pkl-go-examples
* https://github.com/apple/pkl-jvm-examples
* https://github.com/apple/pkl-k8s-examples
* https://github.com/apple/pkl-swift-examples
To try out Pkl locally, try downloading our CLI by following our
installation guide. Additionally, try installing one of our various
editor plugins to get a glimpse of what it's like to write Pkl
yourself.
We're so excited to share Pkl with you, and we are just getting
started. We are looking forward to seeing what you might do with it!
Copyright (c) 2024 Apple Inc. All rights reserved.