Dhall: Haskellの新たなキラーアプリ

syocy氏(以下、syocy):では続けて、「Dhall: Haskellの新たなキラーアプリ」という発表をさせていただきます。

繰り返しになりますが、このスライドおよびソースコードはGitHubで管理しています。PDFはGitHub Releasesのほうに上がってます。スライド中のDhallのコードはすべて公式フォーマッタをかけたものになっています。

まず、「設定ファイルってなんでしょう?」という哲学的な話から入りたいと思います。

設定ファイルがなんで出てくるかというと、コンパイルとかいろんな前処理をせずにプログラムのパラメータを変えたい。そのパラメータをソースコードと切り離したいとなると、設定ファイルという概念が出てくるわけですね。

しかしながら、既存の設定ファイル言語にはいくつか不満があります。現在主流の設定ファイル言語は、機能が十分でないところがけっこうあります。何かというと、同じ値を繰り返し書きたくないという「Don’t Repeat Yourself」というプログラマの原則がよく言われると思うんですけど、設定ファイル言語においてはリピートが必要になってくることがけっこうあると。

あと、入ってはいけない変な値が入っていたら教えてほしいんだけれども、既存の設定ファイル言語はあまりそうなっていない。つまり、静的検査とか型とか、そういうのはあんまりないみたいな感じです。

設定ファイルが大きくなってくると、「分割して、いくつかの設定ファイルから1つの設定ファイルを作りたい」みたいな欲求が出てくるわけですけど、そういうのをサポートしている設定ファイル言語はあまりない。

こんな感じのたくさんの機能要望があるわけですが、ただし、設定ファイルとしての領分は守ってほしいという、若干相反するような要望もあります。

何かというと、例えば、設定ファイル言語が副作用を持っていて動作させないと何が起こるかわからないとかや、設定ファイル中に無限ループが書けてしまって設定ファイルを読もうとしただけでハングするとか、そういうことは避けたい。何かいいものはないのか? そこで出てくるのがDhallです。

Dhallとはなにか

Dhallは今まで挙げたすべての特徴を備えた設定ファイル言語です。つまり、型と関数とインポートの機能があります。そして、無限ループは起こりません。チューリング完全ではないです。さらに、副作用は標準のDhallでは存在しません。

読みは「dɔl」、この発音記号の読み方がわからないですけど、こんな感じです。カタカナでは「ダール」もしくは「ドール」だと思います。このスライドでは「ダール」で通させていただきます。

まずDhallの開発体制なんですけど、言語仕様は特定の実装に依存せずに、独立して管理されています。dhall-langという、GitHubのコミュニティでしたっけ? GitHubの組織の下にdhall-langというリポジトリがあって、そこで管理されています。その上で、主要ツールおよび言語仕様の参照実装はHaskellで書かれています。これはdhall-langの下にあるdhall-haskellというリポジトリで管理されています。

Dhall、型ってちょっとさっき言いましたが、どんな型を持っているかを見ていきたいと思います。

まず、プリミティブな型としては、Bool、Natural、Integer、Double、Textという、真偽値とか整数とか浮動小数点数とか、あと文字列とか、そういうのがあります。一般的な感じじゃないでしょうか。

ですけれども、おすすめの方法なのですが、数値の型はできるだけNaturalを使うのがおすすめです。Naturalは一番使える標準関数が多いですので、使い回しがよいです。

さて、次に複合型です。

ComplexTypeですね。型を引数にとる型みたいな感じです。それにはListとOptionalとRecordとUnionがあります。

Listはリストですね。Optionalは0〜1個の値をとるものです。最近の言語ではOptionとかMaybeとかって言われてるやつだと思います。Recordというのは、だいたいJSONのオブジェクトに相当するもので、key-valueのペアの集まりです。Unionというのもあって、これは「このうちのどれか1つの値が入る型」みたいなものになります。

Unionの値の例のところを見ていただけるとわかると思うんですけど、ちょっと書き方が特徴的ですね。それはDhallの公式の人もわかっているみたいで、記述をサポートするための標準関数があるので、まずはこの記法はあまり責めないでください。この値の例にあるUnionの意味としては、AというラベルとBというラベルがあるUnion型で、Aを選んだときの値」みたいな意味になります。

インポート機能があります。

インポートは、ローカルのパスとURLからのインポートができます。ローカルのパスは、「./」とやって、あとパスを書くと、それはローカルパスからのインポートとしてDhallは判定して、そこにあるDhallファイルを読みにいきます。また、URLからのインポートもできて、それはhttpまたはhttpsから始まるものであったら、そこはURLからのインポートだとDhallは判定してやっていきます。

インポートにあたってハッシュ値のチェックを設けることができて、それによって「インポートするファイルが変わっているか」「いないか」みたいなチェックをすることもできます。

また、Dhallファイルではない生のテキストのインポートもできます。長いライセンス文章とか、自然言語文章をDhallファイル中に置きたいとなったときは、その機能を使えばOKです。

Dhallの導入について

きっと「なるほど、Dhallは便利かもしれない。いいものかもしれない」となっていただけたと思うんですけど、「自分の使っている言語にはバインディングはないだろう」、また「すでにYAMLやJSONで設定ファイルを書いてしまっているし……」みたいなことをみなさん思われるんじゃないでしょうか。

実際、いまのところ、最新仕様に準拠したバインディングはHaskellのものしかないです。ScalaとRustがちょっと古い仕様に準拠したバインディングがあるぐらいですね。ですが、本当に導入は難しいのか? 

Dhallの導入は簡単です。なぜかというと、dhall-to-yamlというのとdhall-to-jsonというのが提供されています。これは何かというと、DhallファイルをYAMLやJSONに変換するコマンドラインツールです。

つまり、お使いのプログラム言語の中でDhallを読むことを考えなくてもOKです。バインディングがない言語でもDhallを使えます。既存のYAMLを読む処理があったら、もうそれだけでOKになります。

導入もMacとLinuxでは簡単で、MacだとHomebrewで入ります。Linuxだとcurlコマンドで入りますね。WindowsはHaskell環境を入れる必要があるんですけど、それでもコマンドを何個か打てば入りますって感じです。

KubernetesのYAMLを書いてみる

例として、KubernetesのYAMLを書いてみるということをやってみたいと思います。

最近いろいろ話題のOSSのKubernetesというのがありますけど、これはYAMLを大量に用いることで有名です。実際に「Wall of YAML」「YAMLの壁」って言われているらしいですね。

本当にさまざまなYAML管理ソリューションがもうすでに提案されて存在しています。ですけれども、今回はDhallのdhall-to-yamlを用いて、安全かつ便利にKubernetesのYAMLを生成してみることにトライしていきたいと思います。

実はすでに「dhall-kubernetes」というのがあって、本当にKubernetesでYAMLを作るんだったらそれを使うほうがいいんですけど、今回はあくまで例題としてDIYでいきます。

まず、目的とするYAMLはこれです。これはKubernetesの公式サイトに載っているYAMLの例です。まずkindというキーの下にServiceとかデプロイメントとかの値のどれかが入っていると。apiVersionがv1とかなんか入ると。metadataはオブジェクトになっていて、nameというキーを持つなどあると。こういうYAMLがあるというわけですね。これをservice.yamlとします。

まず、愚直にDhallを書いてみます。

まずは、今までDhallには型があるとかいろいろ言ってきましたけど、とくにあまり意識せずに書いてます。kindは”Service”という文字列だと。apiVersionは”v1”という文字列だと。ほとんどそのままYAMLとそんな変わらない感じで書けます。

dhall-to-yamlを使って変換した結果がこちらとなります。キーの順番とかは入れ替わってますけど、同じYAMLができました。

型などを意識しなくても、目的とするYAMLはできます。ユースケースによっては、これぐらいでもまあまあ便利です。

さらにDhallの能力を引き出そうとするならば、型やデフォルト値などを用意することができます。基本的にはアイデアとしては、Union型を用いて記述できる値を制限する。Record型を用いてデフォルト値を用意するということをやっていきます。

さて、これがKubernetesのYAMLの型を定義するファイルの一部です。まずKindというのは、ServiceもしくはPod、もしくはDeploymentのどれかであるというUnion。ApiVersionは、”v1”という値を持つのであろうUnion。あとは、MetadataとかSelectorとかいろいろあると。いろんなRecord型があるというふうに定義していきます。

これUnionで表現して、ApiVersionとかはUnionで表現しているんですけれども、これらは単にDhall上の値でしかないので、YAMLのStringに戻す処理が必要です。そこで、makeYamlという関数を作ります。それでさっきのapiVersionとかのUnionをStringに戻します。実装はちょっと長くなっちゃうので省略します。

定義した型を利用して書き直す

さて、先ほどの愚直に書いたやつを、型を利用して書き直してみましょう。

まず、1行目で型を定義したファイルを読み込んで、「k」という名前にします。3行目で、先ほど作ったmakeYamlという関数を呼んでいます。makeYamlの引数が、4行目以降ですね。kindは、kindのうちServiceというやつ、apiVersionはapiVersionのうちv1というやつ、みたいな感じで指定していきます。

それで、先ほどと同じようにdhall-to-yamlにかけるんですけど、結果は同じで。ただし、より安全にKubernetesのYAMLが表現できました。

例えば、これをServiceという文字をタイポしているとか、TCPという文字をTPCにしてるとか、そういう間違いを犯す可能性が最初のバージョンではあったんですけど、ここではもうそういった間違いを犯す可能性はなくなりましたというわけですね。

デフォルト値。これでもそれなりにいいんですけど、Kubernetes YAMLのデフォルト値を作ってみましょう、ということをやります。

型定義は、先ほど同じやつをk_types.dhallというのをそのまま使って、3行目・4行目はdefaultServiceを定義しています。Serviceなので、kindはService固定で、apiVersionはv1固定だとなります。6行目でtcpというRecordを定義しています。これはProtocols.TCPを持つポートだというわけですね。

デフォルト値を用意しておくと、同じような値をたくさん作るときに間違いをしにくくなるので便利です。ただし今回は紙幅の都合上、1つだけの場合を例示します。

それをどう使うかというと、こんな感じです。

k.makeYamlというところはさっきと同じですけど、defaultServiceというのを次の2行目で使っていて、それに「∧」の演算子でRecordを合成します。defaultServiceの値をそのまま使って、metadataとかspecとか、defaultServiceに定義されていないものを入れていきますという感じです。

Recordを合成する演算子はいくつかあるんですけど、ここではこの「∧」を使っているという感じです。

これも先ほどと同じように、dhall-to-yamlにかけると、同じYAMLが出てきますというわけですね。

さて、早いですが、まとめですね。

Dhallは設定ファイルとして限界を突き詰めた言語です。インポートや型とか関数とかの機能を持っています。それでありながら、副作用とか無限ループなどの危険はありません。

YAMLはJSONに変換できますので、既存の資産に組み入れることも容易です。とくにMacとLinuxはバイナリ配布があって、より導入しやすいです。

さらに、お使いいただくならば、ユースケースに合わせたレベルで利用することができます。インポートとデフォルト値があればいいだけの人もいると思いますし、ばりばり厳密に型を定義して誤りがないようにしたいという人もいます。それぞれのユースケースに合わせたレベルで利用することができます。

Dhallのより高度な使いかた

まとめは以上ですが、まだ時間がありそうなので、ちょっと補遺のスライドをやっていきます。

「より高度な使い方」。dhallコマンドラインツールとか紹介しなかった言語機能とかやっていきます。

まず、dhallコマンドラインツール。このスライドではもっぱらdhall-to-yamlとかdhall-to-jsonをそのまま使っていきました。これは導入しやすいというのが大きな理由ですが、そのままそれだけで使ってきました。

今のところHaskell環境が必要になるんですけれども、dhallコマンドというのもあって、それを導入するとより幸せになることができます。

これはごく一部のサブコマンドなんですけど、「dhall format」というサブコマンドは公式のフォーマッタで、最近の言語にありがちな公式フォーマッタですね。私はDhallを書くときは、Dhallファイルを保存するときにdhall formatを自動的に走るようにしています。

「dhall repl」というサブコマンドもあって、これはDhallのインタラクティブ環境ですね、「この値がどうなっているか」みたいなのを手元で簡単に確かめるときには、これを使うと便利です。

紹介しなかった言語機能

これはあまり詳しくないのでそんなにないんですが、紹介しなかった言語機能として、Dhallは「多相な関数」とか「型の型」みたいなのもサポートしています。

多相な関数というのは、要するに複数の型の値を取りうる関数みたいな感じですね。ただし、Dhallの特徴として、いわゆる高度な型推論みたいのはほとんどありません。「それでどうやって多相な関数をやるんだよ?」というと、関数に型を渡します。引数として関数に型を渡すという感じで、多相な関数を実現します。

あと、型の型(カインド)。これは最近追加されたんですけど、型の作る型みたいな単位も普通に扱えるようになりました。

HaskellでDhallを直接読み込む。このスライドではもっぱらdhall-to-yaml・dhall-to-jsonを使って、DhallをYAMLとかJSONに変換する方法を見てきました。しかし、Haskellにはバインディングがあるので、YAML等を介さずにDhallファイルをHaskellの型として読み込むこともできます。

これはDhall、YAMLやJSON以外の形式に変換したい場合とか、あるいはHaskellプログラムの設定ファイルとしてそのまま使うみたいな使い方もできるわけですね。

HaskellでDhallを拡張する。HaskellによってDhallのビルトイン関数を追加することができます。標準のDhallにはできないダーティなことがそれによってできるかもしれません。例えばIO副作用をしてしまうとか、そういうことができるかもしれません。

ただし、標準ツールチェインの恩恵を受けられなくなるので、そこは注意が必要です。例えば、dhall formatとかが正しくフォーマットしてくれるかもわからないし、dhall replが正しくそれを評価してくれるかもわからないみたいな状態になります。それに合わせて標準ツールチェインも改造すればいいっちゃいいんですけど、それはちょっと大変です。

事例紹介

時間が最後までいったので、事例紹介です。

これは言っていいと言われていますので言いますが、所属している会社でDhallを使ってみましたという紹介です。会社で混沌のJSONが跋扈してまして……。

(会場笑)

何を以って混沌とするかというと、1〜3文字ぐらいの略語のキーしか使われていないみたいなJSONって、「俺はいったい何書いてんだ?」みたいになるわけですけど。

さらに、ヒントのない列挙型の値みたいなのを使って1とか2とか3とか書くんだけど、「この1とか2とか3ってどういう意味だ?」みたいになると。あとさらに、これJSONですらないんですよね。独自マクロがあるんですよ。

(会場笑)

JSONのフォーマットとしてinvalidになるというひどい独自マクロもあったというわけなんですけど、これをがんばってDhallで全部定義し直して、上記の問題を解決しました。invalid JSONを作る必要があるので、dhall-to-jsonをそのまま使えないんですよね。そこでHaskellで変換処理を書いたというエピソードがあります。

(会場笑)

ちょっと時間余っちゃいましたけど、終わりですね。

司会者:ありがとうございました。

(会場拍手)

Dhallの関数をJSONに変換できるか?

司会者:質問のある方はいらっしゃいませんか?

質問者1:すいません。純粋に話に置いていかれたので教えてほしいんですけど、makeYamlって定義したじゃないですか。あれはdhall-to-jsonでJSONにもできるんですか? そのへんがちょっとわからなかったんですけど。

syocy:ええっと……?

質問者1:これDhallファイルじゃないですか。このファイルにdhall-to-jsonをかけると何が起こるんですか?

syocy:あっ、makeYaml自体をJSONにできるかってことですか? それはサポート外ですね。

質問者1:なるほど。じゃあこれはもうdhall-to-yamlしかできないというDhallファイルですか?

syocy:そうですね。

質問者1:なるほど、ありがとうございます。

syocy:Dhallの関数をJSONとかに変換することはできなかったはずです。エラーになるはずです。

司会者:なので、ここではmakeYamlが返したDhallのRecordをJSONに変換するかたちになります。

syocy:そういうことですね。

司会者:はい。ほかに質問がありますか? ありがとうございます。

質問者2:プログラミング言語としてDhallを見たときに、「ここがもう少しこうだったらいいのにな」と思っていることとかってあるでしょうか?

syocy:OptionalとListのリテラルが同じなんですよ。括弧で書いて、「その鍵括弧はListである」とか「この鍵括弧はOptionalである」みたいな型注釈を入れないといけなかったんですよね。それはかなり大変でした。

ですけれども、一番最近の修正でそこにだけ型推論が効くようになったんですよね。Optional、sumとかNumとか書くと、そこに型推論が効いて、「これはOptionalの値だ」みたいなことをやる。そういう修正が入ったので、今はあまり不満がないですね。

司会者:ほかにありますか?

YAMLファイルをDhallの形式に直す

質問者3:大量のYAMLファイルとかがいま実際あったりすると思うんですけど、それを逆にDhallにするときにはどうやってやるんですか? 要は、dhall-to-yamlとかdhall-to-jsonはあるんですけど、逆はどうやってやるんでしょうか?

syocy:それは、YAMLファイルをDhallの形式に直すということですか?

質問者3:そうですね。

syocy:もしくは、そのYAMLファイルから自動的にDhallの型みたいなのを……。

質問者3:そうですね。自動的にそういうかたちにしてくれるような、何か素敵なツールがあるとうれしいんですけど(笑)。

syocy:今のところ、そういうのはないですね。

質問者3:なるほど、そういう動きは、世の中的にもないんですか?

syocy:たぶんその動きはないと思います。今のところ、がんばって、ただJSONの型を人間が読み取って型をつけていくみたいな感じです。

質問者3:そうですよね。ありがとうございます。

司会者:きっと職人芸が求められる感じじゃないですか。ほかに質問のある方はいらっしゃいますか? ありがとうございます。

ビルトイン関数について

質問者4:「httpとかhttpsで外部から引っ張ってこれる」みたいなのが最初のほうであったじゃないですか? あれって、例えば認証が必要なところはどうするんですか?

例えば、みなさんアプリケーションコードは最悪パブリックで扱っていても、普通コンフィグファイルだけはプライベートで使うじゃないですか。そういうときに引っ張ってこようと思ったときはどうやればいいのかなと思って。

syocy:もしかしたらサポートがあったかもしれないけど、そこはちょっとわからないですね。すみません。

質問者4:例えば、Kubernetesの型の定義だけは外から引っ張ってきて、値の部分を自分で書くというようなイメージでああいうものは使うんですか? 「なんでhttpとかhttps使って、コンフィグを外から引っ張ってくる必要があるのかな?」と思ったんですけど。

syocy:あまり説明しなかったところではあるんですけど、そういうオフィシャルに定義されているKubernetesの型みたいなのをそのまま自分のプログラムで使いたいときとか。あとはPreludeというか、便利関数集みたいなやつも実はDhall公式で公開されてるやつがあって、そちらで定義されている関数をちょこっと使いたいみたいな、そういう時にも外から拾ってくるということをおすすめします。

質問者4:ある種のパッケージマネージャーの代わりみたいとして扱うんですか?

syocy:ああ、そういう言えるかもしれないですね。

質問者4:どうもありがとうございます。

司会者:実運用として、おそらくインターネットにつながらない環境からやるみたいなこともあると思うので、そういう場合はやっぱりいったん保存して、ファイルシステムから読むみたいなことをしたほうがいいかなと思います。

ほかに質問はありますか?

質問者5:すいません。ビルトイン関数を作れるみたいな話があったんですけれども、たぶんゴリゴリ書いてると同じようなやつが出てきて面倒くさいというケースが多いのかなと思うのですが、そういうときは何か簡単に追加できたりできるんですか?

syocy:ビルトイン関数で作るのは、いまのところ、たぶんそこまで簡単というほどでもなくて。Dhall上で済む関数だったらDhallで書いたほうがよいと思います。

質問者5:じゃあ、「なんか繰り返しが多いな」ってときも、そのDhall上の関数でなんとかなっちゃうんですか?

syocy:Dhall上でも繰り返し処理は書けます。先ほどちょっと言ったPreludeに list/mapというやつがあるんですけど、それは、普通のリストに対してマップ処理をかける関数があったりします。

質問者5:ありがとうございます。

司会者:では、ちょっと早いですが、これで終了したいと思います。ありがとうございました。もう一度拍手をお願いします。

(会場拍手)