9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

D言語Advent Calendar 2021

Day 23

D言語はメタプログラミングが強いので便利なライブラリが作れる例

Posted at

はじめに

みなさんメタプログラミングしてますか?

やりすぎは良くないとか言われますが、人に止められるからこそやりたくなりますよね。(カリギュラ効果)

メタプログラミングはD言語の強みでもあり、やるからには徹底的にということで、今回は「他の言語では実現が難しそうだな」と勝手に思っている例として自作のライブラリを1つご紹介します。

どういった言語機能が活用されているのかを少し整理し、今後読者の方の問題解決や何かのアイデアにつながれば幸いです。

lantern

まずは作ったライブラリを簡単に紹介します。lantern というライブラリで2020年に公開したものです。名前は明かりを灯す照明機器としての「ランタン」から取りました。色々照らしてほしかったので。

元はPythonのPandasというライブラリがあり、そこで使われる describe というお手軽集計用の関数が使いたくて作った半ばクローン的な代物です。

PandasにはDataFrameという表形式のデータがあり、 describe 関数はこのDataFrameから平均などの統計量を一通り計算して返してくれる関数です。一方、DでDataFrameと言える一般的なデータ型はないので、「構造体の配列」などを渡すとそこからフィールド毎に要約統計量を計算して返したり、テーブルっぽく標準出力に表示したりできるようにしています。

今回のライブラリでは重要な目標として「雑に使える」ということを掲げており、メタプログラミングを使ってかなりシンプルに使えるようにしつつ、実行時オーバーヘッドを減らすことにも気を配ってみました。

具体的なところで言うと、渡された引数に対して「統計量の計算ができそうなフィールドがあるか」「集計できるならどういった集計をするか」というロジックを__型定義から読み取ってコンパイル時に決定する__という機能を持っています。考えるところはほとんどコンパイラにお任せ。これぞ怠惰ですね。

使用例

説明が難しいので、実際どうなるか例を挙げると以下のような感じです。

まず適当な型とその配列を用意します。(配列でなくとも列挙できるRangeならOK)

struct Person
{
    string name;
    int age;
}

Person[] ps;
ps ~= Person("Alice", 12);
ps ~= Person("Bob", 13)];

そして統計量を取るには describe を一発呼び出すだけです。

import lantern;

auto stats = describe(ps);

assert(stats.name.uniq == 2); // nameは2種類
assert(stats.age.min == 12); // ageの最小値は12

他にもデータ数が取れたり色々ありますが、「とりあえず describe を呼べば動く」という感覚を大切にしています。これなら簡単に使えそうですよね?

しかし、これをコンパイル時に色々やるのはいくつかのハードルがあります。D言語だから簡単にできるんですけどね!

処理内容

処理のステップを整理するとざっと以下の5つからなります。手続き的に実現しようと思えばこれが順当な流れだと思います。

  1. 引数から要素型を求める
  2. 要素型からフィールドの一覧を取り出す
  3. フィールド毎にループし、集計できそうなものに絞りつつ集計方法を決定する
  4. 要素毎かつフィールド毎にループし、値を取り出して集計する
  5. 集計結果の型を生成して結果を詰めて返す

1つ1つの機能を説明すると長くなるので、キーワードだけ出すと以下の言語機能や標準ライブラリで実現できます。(他にもちょっと使っているものはありますが)

利用する言語機能

  1. std.rangeElementType
  2. __traits(allMembers, T)
  3. static foreachis 式およびデリゲートの組み合わせによる判定(いわゆるコンセプト
  4. static foreach__traits(getMember, obj, name)
  5. string mixin

どこが難しいのか?

以前同じような処理を別の静的型付け言語で作れるか試みたところ、私の知識とスキルで作れそうなのはZigだけでした。(机上論ではNimもできそうなのはわかるので、慣れてる人なら問題ないはず)

そもそも実現不可能だと感じた言語が圧倒的に多いのですが、どこが無理か整理すると__以下の3つの要件を同時に満たすのが困難だ__と言えそうです。

  1. describe 等の関数が外部ライブラリとしてあらかじめ提供される
  2. 集計対象の要素型に一切手を加えない
  3. 処理内容が静的に決定される

このうち1つでも緩和できれば他の言語でも実現し得るということで、一体どういうことか順番に検討してみます。

検討

1. describe 等の関数が外部ライブラリとしてあらかじめ提供される

1は、関数が先に提供される、と当たり前のような話ですが、この前提を崩すというのは「集計したい型に対して集計用の関数を都度書く」ということです。

何故そんな対応が必要かというと、これは言ってしまえば __traits(allMembers, T) が無いことが理由です。渡された引数のフィールド一覧が分からないから関数の中身をフィールド一覧に合わせて実装するしかない、というわけです。C言語なんかはまさにこのスタイルになります。マクロで多少楽にはなりそうですけどね。
しかしまぁ型に合わせて関数を書いてたら元も子もないので、汎用的にできるメタメタな関数を書きたい、というのがメタプログラミングの動機なのでした。頑張っていきましょう。

2. 集計対象の要素型に一切手を加えない

2は、要素型に手を入れないの逆、つまり手を加えることで対応できる場合がある、という話です。

これも __traits(allMembers, T) 相当の機能がないことに起因して、対象となるフィールドが分からない問題です。であれば、関数側から分かる範囲で型にあらかじめ情報を埋めておこう、という対処が考えられます。
しかしこのパターンは結構厄介で「1行かそれくらいなら書けば良いじゃん」と思われがちなんですが、「手を加えられない型を集計できない」という対応不可パターンが残ります。たとえば標準ライブラリの構造体がリストになっている場合や、外部からSDKとして渡された型は自分で加工できなかったりします。集計用に型を作って必要なところだけコピーする、といった逃げ方はありますが、それも結構面倒ですよね。汎用性という点では一歩及ばずという感が否めません。

3. 処理内容が静的に決定される

3は、静的にロジックを決定するという前提を崩す、つまり型から実行時型情報を使って計算するという方針です。

これも __traits(allMembers, T) がないことによるもので、任意の型を受け入れつつフィールド一覧を得る、ということが静的に実現困難なので実行時まで遅延させて対処しようということです。
実行時型情報が使えるという前提も必要で言語を選びますし、当たり前ですが実行時オーバーヘッドがあります。あと集計可能なフィールド有無くらいは静的にチェックしたいですよね。メタメタとはちょっと言い難く、集計できるのは分かるのですが何とも惜しい限りです。

重要な言語機能

というわけで、安全性や速度面も含めて期待値を満たすには __traits(allMembers, T) が意外な立役者であることがわかりました。これがないから似たようなことができないと言えます。これができても、まだ足りない機能がある言語もあるかもしれません。

ちなみにこれはJSON等のシリアライザでも全く同じ構図で、同じようなポイントでつらくなります。C#とかJavaはリフレクションが充実してるので3の手が取れることからこういった分野で強いと言えそうです。

そしてD言語であれば、同等のことが静的にできるのでオーバーヘッドもなく、より高速にできるはず、という期待があります。

allMembers の利用方法

というわけで、立役者である __traits(allMembers, T) の使い方を簡単に説明して終わりにしたいと思います。
細かい仕様は以下をご覧ください。

使い方は難しくなく、ざっくり型を渡せばメンバーの名前が一覧で得られる特殊な関数のようなものです。

使い方
struct Test {
    int a;
    int b;
}

foreach (string name; __traits(allMembers, Test)) {
    writeln(name);
}
結果
a
b

これがかなり強くて、メソッドがあればメソッド名も取れたりします。
これと __traits(getMember, obj, name) という別の機能を組み合わせると任意のフィールドが取り出せます。今回の用途であればもはや楽勝の雰囲気が漂いますね。

更に特殊な用途として、モジュールを渡すと定義されているものの一覧が取れる、というのがあります。
D言語Cookbookに例がありますのでこちらも参照してみてください。

あとは jcli というコマンドラインツールを作るフレームワークでは、モジュールを渡すとコマンド定義があるか探して使うといった機能があります。

色々便利に使えますので、ぜひぜひメタプログラミングに挑戦してみてください。

さいごに

動的型付けの言語では簡単にできることでも、静的型付け言語になると途端に難しい、というケースは多くあります。今回もPythonからの移植チャレンジと言えますが、これがなかなか面白い難度で、メタプログラミングの面白さを思い出しました。

D言語はこういった静的なメタプログラミングが他の言語より強いと言われますが、まさに他の言語では実現が困難と思えることでもあっさりやってのけます。ここまで強いのであればぜひ活用していきたいですよね。

以上、メタプログラミングは楽しいし便利だぞ!!

9
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?