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.