設定ファイルは、アプリケーションの振る舞いをカスタマイズし、環境間での移行を容易にするための鍵となります。
設定ファイルはJSONやYAMLで書くことが主流ですが、新たな選択肢が登場しつつあります!?
それが今回紹介する Pkl です。
Pklとは
Pkl(ピックル)とはApple社が2024年2月に発表したOSSであり、コンフィグのためのプログラミング言語です。
公式ドキュメントによると、Pklを使用することで設定ファイルをより効率的に、そしてより安全に扱うことができ、さらに設定ファイル内のエラーをコンパイル時に検出する能力を持っているため、JSONやYAMLなどの静的な設定ファイルで起こりうる課題を解決してくれることが期待できます。
Pklの特徴
Pklには以下の特徴があります。
- 多様な設定フォーマットを生成することができる
- Pklで書いたファイルをそのままアプリケーションの設定ファイルとして使用することができる
- IDEとの連携
- 検証可能
- 他の言語同様に様々な構文が備わっている
特徴1: 多様な設定フォーマットを生成することができる
PklはCLIを使うことで、Pklで記述したデータをJSON、YAMLなどの形式に変換することが可能となっております。
例として以下のappConfig.pkl
をJSONとYAMLに変換してみます。
typealias LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"
class LogConfig {
logLevel: LogLevel = "INFO"
}
typealias Port = UInt16(this > 0)
class AppConfig {
host: String = "0.0.0.0"
port: Port = 443
logConfig: LogConfig
}
appConfig: AppConfig = new {
port = 8080
host = "127.0.0.1"
logConfig = new {
logLevel = "DEBUG"
}
}
JSONに変換する
> pkl eval -f json appConfig.pkl
{
"appConfig": {
"host": "127.0.0.1",
"port": 8080,
"logConfig": {
"logLevel": "DEBUG"
}
}
}
YAMLに変換する
> pkl eval -f yaml appConfig.pkl
appConfig:
host: 127.0.0.1
port: 8080
logConfig:
logLevel: DEBUG
特徴2: Pklで書いたファイルをそのままアプリケーションの設定ファイルとして使用することができる
特徴1に記述したようにJSONなどの静的ファイルとして吐き出すこともできますが、.pkl
のファイルをそのままアプリケーションの設定として使用することが可能なのもPklの大きな特徴の1つです。
Pklで設定を書いた後に、コード生成ツールを使用することでPklで定義した設定に対応するソースコードを生成してくれるため開発を安全で効率的に進めることができます。
現時点(2024年3月1日時点)で対応している言語は以下の4つとなります。
試しにPklでGoのソースコードを生成してみます。
- Goのプロジェクトを作成する
mkdir -p go-pkl/config/pkl
cd go-pkl
go mod init go-pkl
# PklからGoのソースコードを生成するツールをインストールする
go install github.com/apple/pkl-go/cmd/pkl-gen-go@latest
- 以下の
./config/pkl/appConfig.pkl
を作成する
@go.Package { name = "go-pkl/config" }
module introduction.of.pkl
import "package://pkg.pkl-lang.org/pkl-go/pkl.golang@0.5.3#/go.pkl"
typealias LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"
class LogConfig {
logLevel: LogLevel = "INFO"
}
typealias Port = UInt16(this > 0)
class AppConfig {
host: String = "0.0.0.0"
port: Port = 443
logConfig: LogConfig
}
appConfig: AppConfig = new {
port = 8080
host = "127.0.0.1"
logConfig = new {
logLevel = "DEBUG"
}
}
-
./config/pkl/appConfig.pkl
からGoのソースコードを生成する
> pkl-gen-go config/pkl/appConfig.pkl
Generating Go sources for module config/pkl/appConfig.pkl
Determined base path to be go-pkl from go.mod
/path/to/your/go-pkl/config/LogConfig.pkl.go
/path/to/your/go-pkl/config/init.pkl.go
/path/to/your/go-pkl/config/loglevel/LogLevel.pkl.go
/path/to/your/go-pkl/config/Pkl.pkl.go
/path/to/your/go-pkl/config/AppConfig.pkl.go
- 以下のコードが自動生成される
// Code generated from Pkl module `introduction.of.pkl`. DO NOT EDIT.
package config
type AppConfig struct {
Host string `pkl:"host"`
Port uint16 `pkl:"port"`
LogConfig *LogConfig `pkl:"logConfig"`
}
// Code generated from Pkl module `introduction.of.pkl`. DO NOT EDIT.
package config
import "github.com/apple/pkl-go/pkl"
func init() {
pkl.RegisterMapping("introduction.of.pkl", Pkl{})
pkl.RegisterMapping("introduction.of.pkl#AppConfig", AppConfig{})
pkl.RegisterMapping("introduction.of.pkl#LogConfig", LogConfig{})
}
// Code generated from Pkl module `introduction.of.pkl`. DO NOT EDIT.
package config
import "go-pkl/config/loglevel"
type LogConfig struct {
LogLevel loglevel.LogLevel `pkl:"logLevel"`
}
// Code generated from Pkl module `introduction.of.pkl`. DO NOT EDIT.
package config
import (
"context"
"github.com/apple/pkl-go/pkl"
)
type Pkl struct {
AppConfig *AppConfig `pkl:"appConfig"`
}
// LoadFromPath loads the pkl module at the given path and evaluates it into a Pkl
func LoadFromPath(ctx context.Context, path string) (ret *Pkl, err error) {
evaluator, err := pkl.NewEvaluator(ctx, pkl.PreconfiguredOptions)
if err != nil {
return nil, err
}
defer func() {
cerr := evaluator.Close()
if err == nil {
err = cerr
}
}()
ret, err = Load(ctx, evaluator, pkl.FileSource(path))
return ret, err
}
// Load loads the pkl module at the given source and evaluates it with the given evaluator into a Pkl
func Load(ctx context.Context, evaluator pkl.Evaluator, source *pkl.ModuleSource) (*Pkl, error) {
var ret Pkl
if err := evaluator.EvaluateModule(ctx, source, &ret); err != nil {
return nil, err
}
return &ret, nil
}
// Code generated from Pkl module `introduction.of.pkl`. DO NOT EDIT.
package loglevel
import (
"encoding"
"fmt"
)
type LogLevel string
const (
DEBUG LogLevel = "DEBUG"
INFO LogLevel = "INFO"
WARN LogLevel = "WARN"
ERROR LogLevel = "ERROR"
)
// String returns the string representation of LogLevel
func (rcv LogLevel) String() string {
return string(rcv)
}
var _ encoding.BinaryUnmarshaler = new(LogLevel)
// UnmarshalBinary implements encoding.BinaryUnmarshaler for LogLevel.
func (rcv *LogLevel) UnmarshalBinary(data []byte) error {
switch str := string(data); str {
case "DEBUG":
*rcv = DEBUG
case "INFO":
*rcv = INFO
case "WARN":
*rcv = WARN
case "ERROR":
*rcv = ERROR
default:
return fmt.Errorf(`illegal: "%s" is not a valid LogLevel`, str)
}
return nil
}
このようにGoの場合はPklで定義したデータ型に対応する構造体だけでなく、設定ファイルをパースする関数まで自動生成してくれるみたいです。(便利!)
特徴3: IDEとの連携
PklはVSCode、IntelliJ、 Neovimなどに対してプラグインを提供しているため、他の静的型付け言語を書いているのと同じ感覚でIDEのサポートを受けることができます。
各プラグインの詳細について知りたい場合は以下のリンクを参照してください。
※試しにVSCodeのプラグインを入れてみましたが、現時点(2024年3月1日)では、.vsix
ファイルから手動でインストールする必要があるみたいなので若干の手間がかかりました。
特徴4: 検証可能
作成した.pkl
を事前に検証することができるので、より安全、確実に使用することができるという特徴もあります。
試しにappConfig.pkl
のlogLevel
に許容されていないHOGE
を指定してみた時の挙動を確認してみます。
typealias LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR" // Emun
class LogConfig {
logLevel: LogLevel = "INFO"
}
typealias Port = UInt16(this > 0) // 0より大きくて16ビット以下の数値
class AppConfig {
host: String = "0.0.0.0"
port: Port = 443
logConfig: LogConfig
}
appConfig: AppConfig = new {
port = 8080
host = "127.0.0.1"
logConfig = new {
logLevel = "HOGE" // <- ここ
}
}
以下のコマンドを実行するとエラーが発生します。
> pkl eval AppConfig.pkl
–– Pkl Error ––
Expected value of type `"DEBUG"|"INFO"|"WARN"|"ERROR"`, but got `"HOGE"`.
1 | typealias LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at AppConfig#LogConfig.logLevel (file:///path/to/your/AppConfig.pkl, line 1)
20 | logLevel = "HOGE"
^^^^^^
at AppConfig#appConfig.logConfig.logLevel (file:///path/to/your/AppConfig.pkl, line 20)
106 | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.25.2/stdlib/base.pkl#L106)
エラーメッセージからもHOGE
はLogLevel
型ではないと怒られてるのがわかりますね。
特徴5: 他の言語同様に様々な構文がある
Pklは、他の言語と同様にforやwhen(if)、class、functionなど多くの構文が標準で備わっています。
試しにPklでFizzBuzz問題を実装してみます。
typealias FizzBuzzValue = Int|"FIZZ"|"BUZZ"|"FIZZBUZZ"
class FizzBuzzItem {
value: FizzBuzzValue
}
function fizzbuzz(_num: Int): FizzBuzzItem = new {
when (_num % 15 == 0 ) {
value = "FIZZBUZZ"
} else {
// else-ifがない?のでネストが深くなった
when (_num % 3 == 0) {
value = "FIZZ"
} else {
when (_num %5 == 0) {
value = "BUZZ"
} else {
value = _num
}
}
}
}
// for i := 0; i < 100; i++ みたいな書き方ができない...
local nums = List(10,11,12,13,14,15)
answer: Listing<FizzBuzzItem> = new {
for (_num in nums) {
fizzbuzz(_num)
}
}
YAMLで出力する
> pkl eval -f yaml fizzbuzz.pkl
answer:
- value: BUZZ
- value: 11
- value: FIZZ
- value: 13
- value: 14
- value: FIZZBUZZ
まとめ
今回はPklについて調べてみたことや実際に使ってみた感想などを紹介しました。
初期の導入/学習コストさえ払えばロバストに効率よく設定ファイルを作り込むことができるため結構便利だと思いました。なので皆さんも興味があれば一度使ってみるのはどうでしょうか?(最悪YAMLなどにリプレイス可能なので取り敢えず導入してみるのもありかもしれません)
Pklはまだ世に出てまもない言語なので今後の動向にも注目ですね。