まだ設定ファイルをJSONやYAMLで書いてるの？


設定ファイルは、アプリケーションの振る舞いをカスタマイズし、環境間での移行を容易にするための鍵となります。

設定ファイルはJSONやYAMLで書くことが主流ですが、新たな選択肢が登場しつつあります!?

それが今回紹介する **Pkl** です。

## Pklとは

[Pkl(ピックル)](https://github.com/apple/pkl?tab=readme-ov-file)とはApple社が2024年2月に発表したOSSであり、コンフィグのためのプログラミング言語です。

[公式ドキュメント](https://pkl-lang.org/)によると、Pklを使用することで設定ファイルをより効率的に、そしてより安全に扱うことができ、さらに設定ファイル内のエラーをコンパイル時に検出する能力を持っているため、JSONやYAMLなどの静的な設定ファイルで起こりうる課題を解決してくれることが期待できます。

- インストール方法
  - https://pkl-lang.org/main/current/pkl-cli/index.html#installation

## Pklの特徴

Pklには以下の特徴があります。

- 多様な設定フォーマットを生成することができる
- Pklで書いたファイルをそのままアプリケーションの設定ファイルとして使用することができる
- IDEとの連携
- 検証可能
- 他の言語同様に様々な構文が備わっている

### 特徴1: 多様な設定フォーマットを生成することができる

PklはCLIを使うことで、Pklで記述したデータをJSON、YAMLなどの形式に変換することが可能となっております。

例として以下の`appConfig.pkl`をJSONとYAMLに変換してみます。

```pkl:appConfig.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"
  }
}
```

JSONに変換する

```sh
> pkl eval -f json appConfig.pkl
{
  "appConfig": {
    "host": "127.0.0.1",
    "port": 8080,
    "logConfig": {
      "logLevel": "DEBUG"
    }
  }
}
```

YAMLに変換する

```sh
> pkl eval -f yaml appConfig.pkl
appConfig:
  host: 127.0.0.1
  port: 8080
  logConfig:
    logLevel: DEBUG
```

### 特徴2: Pklで書いたファイルをそのままアプリケーションの設定ファイルとして使用することができる

[特徴1](#特徴1-多様な設定フォーマットを生成することができる)に記述したようにJSONなどの静的ファイルとして吐き出すこともできますが、`.pkl`のファイルをそのままアプリケーションの設定として使用することが可能なのもPklの大きな特徴の1つです。

Pklで設定を書いた後に、コード生成ツールを使用することでPklで定義した設定に対応するソースコードを生成してくれるため開発を安全で効率的に進めることができます。

現時点(2024年3月1日時点)で対応している言語は以下の4つとなります。

- [Java](https://pkl-lang.org/main/current/java-binding/index.html)
- [Kotlin](https://pkl-lang.org/main/current/kotlin-binding/index.html)
- [Swift](https://pkl-lang.org/swift/current/index.html)
- [Go](https://pkl-lang.org/go/current/index.html)

試しにPklでGoのソースコードを生成してみます。


- Goのプロジェクトを作成する

```sh
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`を作成する

```pkl:./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のソースコードを生成する

```sh
> 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
```

- 以下のコードが自動生成される

```go:./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"`
}
```

```go:./config/init.pkl.go
// 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{})
}
```

```go:./config/LogConfig.pkl.go
// 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"`
}
```

```go:./config/Pkl.pkl.go
// 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
}
```

```go:./config/loglevel/LogLevel.pkl.go
// 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のサポートを受けることができます。

各プラグインの詳細について知りたい場合は以下のリンクを参照してください。

- [IntelliJ](https://pkl-lang.org/intellij/current/index.html)
- [VSCode](https://pkl-lang.org/vscode/current/index.html)
- [Neovim](https://pkl-lang.org/neovim/current/index.html)

※試しにVSCodeのプラグインを入れてみましたが、現時点(2024年3月1日)では、`.vsix`ファイルから手動でインストールする必要があるみたいなので若干の手間がかかりました。

### 特徴4: 検証可能

作成した`.pkl`を事前に検証することができるので、より安全、確実に使用することができるという特徴もあります。

試しに`appConfig.pkl`の`logLevel`に許容されていない`HOGE`を指定してみた時の挙動を確認してみます。

```pkl
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" // <- ここ
  }
}
```

以下のコマンドを実行するとエラーが発生します。

```sh
> 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など多くの構文が標準で備わっています。

- [公式の言語仕様](https://pkl-lang.org/main/current/language-reference/index.html#for-generators)
- [標準パッケージ](https://pkl-lang.org/package-docs/index.html)

試しにPklで[FizzBuzz問題](https://ja.wikipedia.org/wiki/Fizz_Buzz)を実装してみます。


```pkl:fizzbuzz.pkl
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で出力する

```sh
> pkl eval -f yaml fizzbuzz.pkl
answer:
- value: BUZZ
- value: 11
- value: FIZZ
- value: 13
- value: 14
- value: FIZZBUZZ
```

## まとめ

今回はPklについて調べてみたことや実際に使ってみた感想などを紹介しました。

初期の導入/学習コストさえ払えばロバストに効率よく設定ファイルを作り込むことができるため結構便利だと思いました。なので皆さんも興味があれば一度使ってみるのはどうでしょうか？(最悪YAMLなどにリプレイス可能なので取り敢えず導入してみるのもありかもしれません)

Pklはまだ世に出てまもない言語なので今後の動向にも注目ですね。

## おまけ

[特徴2](#特徴2-pklで書いたファイルをそのままアプリケーションの設定ファイルとして使用することができる)で紹介したGoのサンプルプログラムを[GitHub](https://github.com/gracefulm/go-pkl-sample)に公開しています。
興味があればご覧ください。