Rustと3種のDSL

κeen氏(以下、κeen):今日発表するにあたって事前にアンケートを取ってきました。「聞きたいのは、開発ハプニング集、使ってる便利ライブラリ集、チームの開発環境の整備?」というのを挙げて取ったんですが、どれもかぶりそうだなと思って、これを全部破棄して発表したいと思います。

(会場笑)

「Rustと3種のDSL」について発表します。私はIdein Inc.というところに勤めています。あとで話しますが、こんな感じです。

今日はDSLの話をしていくのですが、DSLをみなさんご存じですか? 私がすごく好きな引用があるので引用させてもらいます。Lispの話なんですけど。

「Lispでは、プログラムをただプログラミング言語に従って書くことはしない。プログラミング言語を自分の書くプログラムに向けて構築するのだ」。

要は、プログラムを書くときには、ただ書くんじゃなくて、まずそのプログラムを書くための言語を作ってからちょっとしたプログラムを書いてあげれば、それ簡単にできるよね。しかも変更も簡単だよね、という考え方があります。

DSLというのはドメイン特化言語の略です。Domain Specific Languageですね。自分の解きたい問題に合わせてミニ言語を作ります。

このメリットは、関心に集中ということと、隠れたメリットとして、ドメイン特化言語を設計するにあたってドメインについて深く知らないといけないので、ドメインに対する考察が深まります。「あっ、これ考慮できてないじゃん」といったのがよくわかります。

2つのドメイン特化言語

ドメイン特化言語といったときに、大きく分けて2種類あります。内部DSLと外部DSL。今回は3種類あるので、どちらかが2つかぶっているってわけですね。

内部DSLというのは、ホストの言語の式でそれっぽく作る。ホスト言語ってわかりますか? 今回のホスト言語というのはRustのことです。外部DSLは、外部化してパーサから作る。独自記法とか導入する感じですね。

こうやって2つあるんですが、まずは内部DSLを検討しましょう。パーサを作るのは面倒くさいです。Rustのパーサはすでにありますという感じですね。

DSLの使いどころはいろいろあります。個人的にはこの条件でいいんじゃないかなと思っています。

素直に書くと記述が多い。でも、関数にまとめようと思ってもバリエーションがあってまとめられない。そういったときにDSLを使うのが一番ちょうどいい解決策なんじゃないかと思っています。

よくあるのが、ビジネスロジックとかがそんな感じですよね。ビジネスロジックって、例えばデータベースアクセスだとかコンテキストからデータを引っ張ってきたりとか、微妙にちょこまかした複雑な部分はあるんですが、関数にしたい部分というのはもうちょっとシンプルなはずで、そういった部分をうまく隠してあげるといいプログラムになるでしょう。

今開発中のものを話しておきます。あとでこれが出てきます。社長がしゃべっているのでたぶん外に出して大丈夫でしょう。Actcastというサービスを開発しています。まだサービスインはしていません。

これは、IoTのデバイスを管理するサービスです。ポイントとしては、人や管理グループ、デバイス、アプリケーションなどエンティティが多いので、ちょっと管理が面倒くさい。もう1個は、Open API、旧称Swaggerを使ってAPIを管理しているということで、これもあとで出てきます。

あるいはこれを開発してくれる人を募集しているので、ぜひみなさん応募してください。

今回これでDSLを3種類作ったので、その話をしていきます。まぁDSLといってもそんなに大した話ではありません。

珠玉の内部DSLについて

まず、珠玉の内部DSL。内部DSLというのが一番推奨の仕方です。

アクセス権限のチェックがあります。

エンティティがいっぱいあるのでめちゃくちゃ権限のチェックがあるんですね。普通にやろうとすると、is_user_readable_to_deviceとか、UserとDeviceが出てきて。これ本当はグループやアプリケーションなどとかいろいろあるんですけど。

簡単にやっておくと、read/write権限があるかどうかをチェックしようと思ったら、まず、readableをチェックして失敗したらread errorを出して、write権限をチェックして失敗したらwrite権限のエラーを出して……というのを延々書かなきゃいけなくなります。まぁ、これは面倒くさいです。

これをDSLで書いたらこうなりました。operatorがis_readable().is_writable().to(device)というふうにやって、そのまま終わるようにしてあげました。

もう1個、2回コネクションを渡してあげているんですね。これが1回で済んでいます。これをどうやったかというと、まずはモデリングをしました。

モデリングが大事です。Precondition、条件チェックをまずtraitにしてあげて。イメージとしては、このPreconditionの実装しているものがこのDSL全体だよ、というイメージです。

例えば、権限チェックなのでAND条件とかありますよね。Andというのを作ってあげて、PreconditionをAndに実装してあげて、and_thenなどを使ってやってあげます。

このthenはResultにしているので、はてな記法とかでできますし、あとはエラーがいろんなレイヤーを出していくので、まぁ、それも出せるでしょうって。

あとはIsReadableとかIsWritableを用意してあげて。この型はジェネリクスにしてあげると、例えば、ユーザーがデバイスをreadできるだとか、ユーザーがアプリをreadできるだとか、グループが特殊なアプリケーションをreadできるだとか、そういったことが汎用的に表せます。

これがモデリングです。さっきのDSLはこのモデリングの上に構築しました。

どうなるかというと、いったんモジュールは切っておいたほうが便利でしょうということで、IsReadableDslというのを作ってあげて、is_readableというメソッドを作ってあげたり、IsReadableDslに対して、これをUserに実装してあげると、is_readableというメソッドが呼べたりします。こうしてDSLを構築しました。

コード全体はGistにあげています。これ自体をそのまま使っているわけではなくて、今回このためにちょっとそれっぽく書き直しました。もう1回あとで書き直すつもりです。

一番これがシンプルな、シンプルというかDSLの正道的な使い方です。トレイトをうまく使ってメソッド記法でそれっぽく書きました。もう1個、演算子オーバーロードを使ってやる方法もあるのですが、あまり悪用しないほうがいいです。たぶんC++というワードを出すとたぶんみなさん納得すると思います。

気をつけてほしいのが、DSLはあくまで略記のための手法であって、まずはモデリングをよくしましょう。まずはこういったPreconditionだとかのモデリングをしっかりした上で、それを便利に書けないかなということで、DSLを構築しましょう。

あと、これはモデルが後ろにあって、表面にDSLがあるので、内部実装を変更することができたりもします。

例えば、Preconditionで、IsReadableでDBにアクセスして、IsWritableでDBにアクセスして、2回アクセスしているんですね。でも、これは同じ権限チェックなので1回にまとめられるはずです。

これをIsReadableWritableというモデルを用意してあげて、フロントエンドの部分でもIsReadableWritable用意してあげると、なんとDBアクセスを1回にできる。ソースコードはぜんぜん変える必要がないというメリットもあります。

諸刃の剣、マクロDSL

次です。諸刃の剣のマクロDSL、actixです。

actixを使っていて、API、appのルーティングをしていたりして、ずらずら〜と書いてあるんですが、実際にそれっぽいことをやっているのはこの部分だけなんですね。ノイズがすごく多いんですよ。

でも、それをまとめられるかというと、いろいろと難しいところがあります。例えばこのpathとかいう、pathのスプライシングがあるAPIとないAPIがあったりします。例えばこのidがないapp全体を取るようなAPIもあるわけで、このpathというのが存在したりしなかったりしてたりとか。

あとは、実はこのPathParametersは今ここで定義してあって、app_idのために定義しています。これはそのpathごとにぜんぜん違うものがやってくるよとか、そういったものがあって関数にはまとめられないんですよね。

どうするかというと、マクロです。マクロでざくっとやってあげて、これだけで済むようにしました。関心のある部分はここだけですね。

あとはPathをルーティングで、path idにAppIdという型をつけてあげて、ルーティングっぽいけど型もついていて、ここから文字列を生成するのと、さっきのPathParametersを生成するの、両方やっています。こんなふうに文字列の生成や構造体の生成など、そういったものはマクロじゃないとできません。

マクロの実装も見せたいのですが、ちょっと多すぎて見せられないというか、見せてもよくわからないなら出す意味がないのですが、300行ぐらいマクロを書いています。

マクロは便利は便利なんですけど、可能なら使わないほういいです。これはLispからの知恵です。なぜならマクロは第1級ではないです。第1級ではないというのは、雑に言うと、関数の引数に渡せないから。そんなに便利ではありません。

ただ、マクロにしかできないこともあります。こういうふうに動的にシンボルを作る……シンボルを作ると言って伝わるのかな。例えば変数名を勝手に作ったり、関数の引数を増やしたり、そういったことができるのはマクロしかないので、場合によってはマクロを使うといいんじゃないでしょうか。

けっこうトリッキーなのですが、『The Little Book of Rust Macros』というサイトがあります。みなさんご存じですか? けっこう長い、「Little Bookなのか?」って感じなんですが、マクロについての書いているドキュメントがあるので、読んでみてください。

ちょっとトリッキーな話をしておきましょうか。

マクロって型ありますね。ttというのを取るマクロを作ってあげて、これを渡してあげるとエラーになります。これって、pathnameなのでコロンで区切られて、tt1個じゃなくて複数のttになるんですね。いいでしょう。

wrap_tyというのがあって、いったんtyで受け取って、そのままパススルーしてあげるとします。そうすると、同じようにこれを渡すと、これは通るんですね。なぜかというと、いったんtyになった時点でこれが1つのトークンツリーになってしまうので、1個扱いになってそのままいってしまうという問題があって、意外と難しい問題です。

もう1個は……ええと、まだ2つ目ですよ(笑)。

(会場笑)

CPSの場合はtake_ttとやって、ネストして呼び出しをしても駄目で。これはCPSのcallbackを受け取れるようにしてあげたら動きます。そういったのがあります。

“みぞの鏡”の外部DLS

最後はみぞの鏡の外部DSL。みぞの鏡をみなさん知ってますか? みぞの鏡(Erised stra ehru oyt ube cafru oyt on wohsi)。読めないですよね。逆から読むと読めます。I show not your face but your heart's desire. ハリーポッターに出てくる「みぞの鏡」ですね。自分の求めているものを映してくれる鏡。

JSON Schemaというものがあって、こんな感じです。

nameとidを定義するだけでこんなに書かないといけません。「なんかこれダサいよね〜」ということで、略記できるツールを作りました。

外部DSLというのはなんでも欲しいものが手に入ります。ただ、これは最後の手段です。パーサを書かなければいけません。シンタックスハイライトありません。互換がありません。エラー出ません。なので、これは最後の手段です。

文字列からRustのデータを構築するというのが外部DSLですね。今回はJSON Schemaがターゲットになっているので、いったんRustのデータを構築してから、もう1回JSONにダンプしています。ある意味ではコンパイラですね。簡単そうですね。最後の手段です、これは。

(会場笑)

まとめです。DSLっていろんなケースで役立ちます。3種のDSLを使いこなそうということで、「珠玉の内部DSL」と「諸刃の剣のマクロDSL」と「みぞの鏡の外部DSL」で3種の神器揃いました。ありがとうございました。

(会場拍手)

APIのドキュメントは出力可能か?

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

κeen:質問の時間はありますか?

司会者:あります。質問ある方いらっしゃいますか?

質問者1:ありがとうございます。2つ目のdef_api!というマクロは、APIのドキュメントを出力することとかできます?

κeen:APIのドキュメントは……がんばったらできるんじゃないですか。やりたくはないです。

(会場笑)

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

κeen:もともとはOpen APIで外部で定義しているものを実装するかたちでやっているので、モチベーションがないというのがあります。

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

質問者2:ぜんぜんRustの話じゃないですけど、最後の外部DSLの話で、そういうことをやる時には、実はJson.NETという便利なものがあって、最近個人的にはまっているんですけど。JSONテンプレート言語なんですけど、それで内部DSLを作ると、わりとうまく書けることがあるという話でした。

κeen:わかりました。ありがとうございます。これはちなみにRustとは関係なくて、Web UIの部分にも参照されるJSON Schemaなので、Rustの内部DSLでは無理だろうということで、こういったものを採用しました。

質問者3:Open API使っているのであれば、Open APIでhyperを出力すればいいのではないでしょうか?

κeen:それ、作ってくださいよ(笑)。

(会場笑)

いや、Open APIのツールでRustを吐けるバックエンドがRust1とRust2というのがあって、私がRust2のほうにコミットしてたんですけど、なかなか直らなくて。まだ吐いたやつがコンパイルに通らないんですよね。なので、まだ待ってください。

司会者:ほかに質問ある方いますか? ではこれで終了とさせていただきます。κeenさん、ありがとうございました。

(会場拍手)