コンフィグレーション生成のための静的型付き言語「Pkl」を試してみる

Vision Pro で盛り上がる最中、Apple からコンフィグレーションファイルを生成するための静的型付言語「Pkl」がオープンソースで公開されたので軽く触ってみました。 github.com

発音は "Pickle" (「ピックル?ピクルゥ?」 )だそうで、 Pickle と聞くと Python"Pickle化" が頭をよぎりますが、今回はそこには触れずに進めていきましょう!

Installation

公式ドキュメントの Installation からインストールします。今回は M1 MacBook Air から試してみるので、 Native macOS executable for amd64 (tested on macOS 10.15) を使います。

$ curl -L -o pkl https://github.com/apple/pkl/releases/download/0.25.1/pkl-macos-amd64
$ chmod +x pkl
$ ./pkl --version
Pkl 0.25.1 (macOS, native)

単に実行ファイルが降ってくるだけなので、実用を考えるなら ./.pkl/bin みたいなディレクトリを作っておいた上でダウンロードして、 .zshrc などに export PATH=$PATH:$HOME/.pkl/bin と追加しておくと任意の場所で実行できるようにできます。(Go風味)

触ってみる

コンフィグレーションファイルを生成するための言語として CUE などが比較にあがりますが、 Pkl がユニークなのはコンフィグレーションファイルの生成だけではなくアプリケーションへ埋め込み可能な言語でもあることでしょう。現状 Language Binding はすでに

  • Java
  • Kotlin
  • Swift
  • Go

がサポートされており、今後も増えていくよと語られています。Go が最初からサポートされているのははっきりとターゲットを意識しているな〜と感じました。 github.com

Swift

せっかくなので Swift で触ってみます。 Quickstart に沿ってやっていきましょう。追加でバイナリが必要なので落としてきます。

$ curl -L https://github.com/apple/pkl-swift/releases/download/0.2.1/pkl-gen-swift-macos.bin -o pkl-gen-swift
$ chmod +x pkl-gen-swift

バージョンが表示されていればOKです。

$ pkl-gen-swift --version
0.2.1

Vapor を使って、簡単なWEBアプリケーションサーバーのプロジェクト用意しておき、そこに pkl-swift を依存として追加します。

// swift-tools-version:5.9
import PackageDescription

let package = Package(
  name: "hello",
  platforms: [
    .macOS(.v13)
  ],
  dependencies: [
    .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"),
    .package(url: "https://github.com/apple/pkl-swift", from: "0.2.1"),
  ],
  targets: [
    .target(
      name: "Generated",
      dependencies: [
        .product(name: "PklSwift", package: "pkl-swift")
      ]
    ),
    .executableTarget(
      name: "App",
      dependencies: [
        "Generated",
        .product(name: "Vapor", package: "vapor"),
      ]
    ),
    .testTarget(
      name: "AppTests",
      dependencies: [
        "Generated",
        .target(name: "App"),
        .product(name: "XCTVapor", package: "vapor"),

        // Workaround for https://github.com/apple/swift-package-manager/issues/6940
        .product(name: "Vapor", package: "vapor"),
      ]),
  ]
)

今回は Example で紹介されているサーバーのホストとポートを環境ごとに用意するユースケースで確認してみましょう。まず AppConfig.pkl というファイルを作り、以下のように書きます。

// AppConfig.pkl
module AppConfig

host: String
// UInt16 は Int(isBetween(0, 65_535)) の typealias として定義されています
port: UInt16

AppConfig モジュールからローカル環境向けのコンフィグを作ります。

// pkl/Local/config.pkl
amends "../AppConfig.pkl"

host = "localhost"
port = 8080

pkl コマンドで正しく設定できているか確認できます。

$ pkl eval ./pkl/Local/config.pkl
host = "localhost"
port = 8080

今回はそのままアプリケーションから読み込んで使いますが、 JSONYAML にも当然変換できますね。

$ pkl eval -f json ./pkl/Local/config.pkl
{
  "host": "localhost",
  "port": 8080
}
$ pkl eval -f yaml ./pkl/Local/config.pkl 
host: localhost
port: 8080

実際に書き出すなら、-o オプション*1をつければいい。

$ pkl eval -f json -o config.json ./pkl/Local/config.pkl

実際にSwift アプリケーションから使えるようにするには pkl-gen-swift で Swift ファイルを生成します。

$ pkl-gen-swift pkl/AppConfig.pkl -o Sources/App/Generated/

すると、以下のようなファイルが生成されます。

// Code generated from Pkl module `AppConfig`. DO NOT EDIT.
import PklSwift

public enum AppConfig {}

extension AppConfig {
    public struct Module: PklRegisteredType, Decodable, Hashable {
        public static var registeredIdentifier: String = "AppConfig"

        public var host: String

        public var port: Int

        public init(host: String, port: Int) {
            self.host = host
            self.port = port
        }
    }

    /// Load the Pkl module at the given source and evaluate it into `AppConfig.Module`.
    ///
    /// - Parameter source: The source of the Pkl module.
    public static func loadFrom(source: ModuleSource) async throws -> AppConfig.Module {
        try await PklSwift.withEvaluator { evaluator in
            try await loadFrom(evaluator: evaluator, source: source)
        }
    }

    /// Load the Pkl module at the given source and evaluate it with the given evaluator into
    /// `AppConfig.Module`.
    ///
    /// - Parameter evaluator: The evaluator to use for evaluation.
    /// - Parameter source: The module to evaluate.
    public static func loadFrom(
        evaluator: PklSwift.Evaluator,
        source: PklSwift.ModuleSource
    ) async throws -> AppConfig.Module {
        try await evaluator.evaluateModule(source: source, as: Module.self)
    }
}

AppConfig.Module 構造体とpklモジュールから AppConfig.Module に評価・変換する関数が含まれていますね。

生成された関数 loadFrom を使って、先ほど作ったローカル環境向けの設定ファイルを読み込めば、意図した環境の AppConfig.Module が得ることができます。あとはそれらをアプリケーションに適用するだけです。

import Generated
import PklSwift
import Vapor

public func configure(_ app: Application) async throws {
  let config = try await AppConfig.loadFrom(source: ModuleSource.path("pkl/Local/config.pkl"))
  app.http.server.configuration.hostname = config.host
  app.http.server.configuration.port = config.port

  try routes(app)
}

swift run で動かせば、 Local/config.pkl で設定した値でサーバーが起動することが確認できるはずです。

[ NOTICE ] Server starting on http://localhost:8080

ざっくり触ってみるところまでやってみましたが、Pkl という言語の表現力にはあまり触れることができませんでした。それはまた別の機会にやってみたいと思います。

言語仕様については紹介しきれない箇所が沢山(throwできたり、Null Value があったりなどなど)あるので、気になる方は公式ドキュメントをご確認ください。

最後に

CUE や toml でいいという声も聞こえてきそうですが、哲学も異なりますし、Framework Integrerations のサポートは Spring (Boot) のみという状況から見ても、Apple の内部のバックエンドサーバー開発のために作ったものを公開したという感じなんですかね(?)。ちなみに公式サイトでは "Incredible IDE Integration" と謳っていますが、 Xcode への統合サポートは現状ありません。Apple 製というのもありしっかり Plist 生成のサポートもあります。今後のアップデートに期待ですね!