Dhallを実戦投入しようとして最終的に断念した話


本記事は[「Develop fun!」を体現する！ Works Human Intelligence Advent Calendar 2023](https://qiita.com/advent-calendar/2023/advent-works-hi) シリーズ2の22日目の記事です。

## 概要

- Dhallという設定記述言語がある
   * 端的に言えば、プログラマブルで型が超厳密なYAML/JSON/TOML
- プログラミングに不慣れなユーザに書いてもらうのにはあまり向いてなさそう
- 熟達したプログラマが使用すればメリットはありそう 
- YAML等ほど汎用的ではないが、使いどころによっては刺さりそう

## はじめに

筆者は最近、業務でDhallという設定記述言語を実戦投入し、それなりのコストを払ったものの、最終的に使用を断念してしまいました。これは筆者の知識不足から、Dhallをその不得意な分野に使用してしまったためだと考えています。

そのため、本記事ではなぜDhallの使用を断念してしまったかについて記しておきたいと思います。

## Dhallとは

 [Dhall](https://dhall-lang.org/) とは、ファイルのインポートや型システム、スクリプト機能が付いた設定記述言語です。
スキーマを型として定義・検証できるほか、YAML等で複雑な設定ファイル等を記述する際に頻出する、一部分だけ変えた繰り返しなどを関数化できるという特徴を持ちます。

Dhallで記述されたコードは各種言語のライブラリを使用して読み込むほか、YAMLやJSONに変換することもできます。

## Dhallを適用しようとしたプロジェクト

私が現在携わっているプロジェクトでは、[Playwright](https://playwright.dev/)を使用したブラウザの自動化によるE2Eテストを実装しています。
このテストでは、非プログラマでもテストシナリオの変更や追加を容易に行えるようにすることを目標としていました。

しかし、Playwrightのテストは、ほぼ生のTypeScriptをそのまま記述する形であり、これを非プログラマが記述するのは困難です。
そこで、細かい実装をTypeScript内部に隠蔽し、シナリオをYAMLでBDD的に記述するDSL[^gherkin]を作成し、実行時にそのYAMLを読み込んでテストを実行するようなフレームワークを作成しました。

[^gherkin]:理想としては、[Cucumber](https://cucumber.io/)の[Gherkin](https://cucumber.io/docs/gherkin/)記法をイメージしていました（カスタマイズ性等の理由から、Cucumber自体の採用はしませんでしたが）

しかし、簡単なテストならば比較的うまくいったのですが、複雑なテストとなると、YAMLの記述が冗長になりがちでした。
具体的には、複数パラメータの組み合わせをすべてテストする必要がある場合に、似たような記述が増えてしまう問題などが生じました。


そこで、複雑なテストはDhall、もしくはDhall上で実装したDSLで書き、YAMLを生成すれば良いのではというアイディアが生まれました。
しかし、このアイディアを試してみたところ、想像以上に困難であることが分かり、断念することとなりました。

以下にその詳細を記します。

## Dhallの使用を断念した主な理由

### 設定ファイルを記述する記法として

Dhallを非プログラマが設定ファイル等を記述するための記法として見たとき、以下のような感想を抱きました。

まず、型付けが厳密すぎることが、非プログラマが設定ファイルを記述する上での障害になっていると感じました。

例として、記述を簡潔にするために、設定ファイル上である設定値を省略可能とし、省略された場合はプログラム側が定めたデフォルト値が設定されるケースは多いと思います。

しかし、Dhallでは省略可能な値はすべて明示的に `Optional`型となるため、デフォルト値以外の値を設定する場合は `Some （値）`、省略する場合は `None （型）` と書かなければなりません。これは`Optional`型等の概念に不慣れな非プログラマにとっては、`Some`とつけなければならない箇所と、つけてはならない箇所が混在するように見え、混乱を招きかねない箇所だと感じます。

また、Dhallには（当然ですが） `Any` のような型がなく、複数の型の値を取りうる箇所は、代数的データ型として定義せざるを得ません。

設定ファイル等では記述のしやすさを重視し、型を動的に変えたいようなケースも多いのですが、YAMLやJSONで単に `"文字列"` や `42` と書くだけでよいような箇所でも、Dhallでは型を動的に変えたいというだけで `Value.Text "文字列"` や、 `Value.Number 42` のような書き方をしなければなりません。

これは特に非プログラマにとって、前述の `Optional` 型の場合と同様、なぜこの記述をつける箇所とつけない箇所があるかといった点で混乱を招くものと考えられます。

また、識別子に非ASCII文字の使用を意図的に禁じているため、特に日本語話者にとって理解しやすいスキーマやDSLを設けることが困難です。

### プログラミング言語として

ある程度の知識を持つプログラマが、他ユーザの設定等の記述を支援する機能を開発するためのプログラミング言語としてのDhallについては、以下の点に改善の余地があるのではないかと感じました。

まず、Dhallには関数として、一引数を取るものしか書けず、複数個の引数を取るためには、無名レコードをad-hocに作るか、カリー化する必要があります。
これは理論的にはきれいなことは理解できますが、実用言語としては書きにくいのは事実です。

また、名前空間がなく、識別子を`/` で区切って名前空間的に使うのが慣例となっています。このため、大規模なコードでは、識別子が非常に長くなりがちです。

そのため、以下の例のように、コメントなしではかなり読みづらいコードになってしまいがちだと感じてしまいました。

```dhall
let script
    : Text -> Script/Config -> TestSuite/List -> Script =
    \(name : Text) ->
    \(config : Script/Config) ->
    \(testSuites : TestSuite/List) ->
    [Map/keyValue Script/MapValue "$config" (Script/MapValue.Config config)] #
    ( List/map TestSuite/MapEntry (Map/Entry Text Script/MapValue)
        (\(e : TestSuite/MapEntry) -> Map/keyValue Script/MapValue e.mapKey (Script/MapValue.TestSuites e.mapValue))
        (TestSuite/List/toWrappedMapList testSuites)
    ) 
```

また、言語自体の大きな特徴として、再帰関数の記述を意図的に禁じている[^recursive]点や、整数について四則演算が揃っておらず、除算などが演算子としてはおろか、組み込み関数等としても存在しない[^div]のも、大規模で実用的なコードを書く上で不自由な点であると感じてしまいました。

[^recursive]: [公式ページ](https://docs.dhall-lang.org/discussions/Safety-guarantees.html)では、安全性や停止性を理由としています。
[^div]:自作は可能ですが、割り算程度から自作しなければならない言語を業務で使うのは正直しんどいです

## Dhallに適した使い方について

本件をきっかけに、Dhallがどのような用途に適するか、またどのような用途には避けるべきかを筆者なりに考えてみました。その結果としては、Dhallが向きそうな用途として

- Dhallの使用を前提として設計した設定ファイル
- スキーマに沿った厳密な検証が必要な設定
- マクロ程度のプログラム記述
- 関数型に慣れたユーザが記述する設定ファイル

といったもの、Dhallが向かなそうな用途として

- 既存の設定ファイルの移植
- 動的な型付けを前提とした設定ファイル
- 凝ったロジックのあるプログラミング
- 非プログラマが記述する必要のあるファイル

といったものが挙げられるのではとの結論に至りました。


## おわりに

Dhallはプログラミング（特にいわゆる関数型プログラミング）に十分習熟したユーザが使用すれば、安全性が高く表現力の高い記法を使用できる優れた言語であると思います。

しかし、今回のケースのように、動的な型付けを前提とし、非プログラマにとっての書きやすさ・理解しやすさを最優先としたいようなケースでは、どうしても採用は難しいものであるという結論となってしまいました。

本記事が、Dhallの名前を聞いたことがあり、現実のプロダクト等で使用したいと考える方々の判断の一助となれば幸いです。