LoginSignup
8
0

More than 3 years have passed since last update.

D言語のUDAを使い倒す

Posted at

概要

  • @xxx のように@を付けて型や変数の前にUDAを付けることができます。将来的な予約語との衝突は気にした方がいいです。
  • UDAを使った処理を書く場合は std.traitshasUDA, getUDAs, getSymbolsByUDA が便利です。
  • ↑が使えない場合は自分で定義します。その際はisXXXX, hasXXXX, getXXXX の3つのテンプレートを定義すると使いやすいです。

UDAとは

UDAというのは、構造体・クラスや関数に独自の属性を付与して、コンパイル時にあれこれしたいときの目印や付属データにするためのものです。
以下のように、 @xxx のように、アットマークを付けてUDAを付与します。

struct testUda{}

// 文字列をUDAとして付与
@("test")
struct Data
{
    // 定義済みの型 testUda をUDAとして付与
    @testUda
    void foo() {}
}

UDAになりうるものは、値や型で、AliasSeqに突っ込めるものは大体突っ込めます。なんでも突っ込めてしまうため、逆に何を突っ込むのがベストなのかですごく悩みます。こういう場合に制約を加えたり、一定のパターンを定石として構築し、考える手間を省くのは良いプラクティスでしょう。

仕様的にはカッコは不要ですが、@nogc導入時のように唐突に予約語と化す可能性があるので、カッコをつけたり、予約語になりにくそうな2語以上の組み合わせでlowerCamelCase化する、UpperCamelCaseやsnake_caseとかにするのが無難です。なお、現時点で@live@futureは予約語になりそうな候補です。

UDAの使い方

UDAの付与の仕方まではいいのですが、いざUDAを利用しようとすると困難が待ち構えています。
普段TMP(テンプレートメタプログラミング)に慣れ親しんでいてもなお、割かし複雑な操作が要求されます。この扱いが難しいため、備忘録的な意味も込めて、本記事の作成に至りました。

さて、UDAを扱う、最もプリミティブなものは以下になるでしょう。

pragma(msg, __traits(getAttributes, Data)[0]);

_traits(getAttributes, identifier) で、identifierに付与されたUDAがTupleで得られます。
詳しくは公式で見てもらうとしても、おおむねこれが仕様のすべてです。

ハイ、このまま「さあ使って!」と言って使える人はレアでしょうね。

UDAを利用するための定石

UDAをうまいこと利用するためには、いくつかの定石を知っておく必要があります。
主にPhobosのstd.traitshasUDA, getUDAs, getSymbolsByUDA あたりを使います。

import std.traits;
struct Data
{
    @("XXX")
    @("YYY")
    int aaabbb;

    @("XXX")
    int aaa;

    @("YYY")
    int bbb;
}
// Data.aaaは、文字列型のUDAを持っていて、1つ目は"XXX"です。
static assert(getUDAs!(Data.aaa, string)[0] == "XXX");
// Data.bbbは"YYY"のUDAを持っています。
static assert(hasUDA!(Data.bbb, "YYY"));
// Dataのメンバで、"XXX"のUDAを持っているメンバは2つです。
static assert(getSymbolsByUDA!(Data, "XXX").length == 2);
// Dataのメンバで、"YYY"のUDAをもつ1つ目のメンバは、aaabbbです。
static assert(__traits(identifier, getSymbolsByUDA!(Data, "YYY")[0]) == "aaabbb");
// Dataのメンバで、"YYY"のUDAをもつ1つ目のメンバ(aaabbb)は、"XXX"というUDAを持っています。
static assert(hasUDA!(getSymbolsByUDA!(Data, "YYY")[0], "XXX"));

ですが、実のところ、ちょっと複雑なことをしようと思うとこれだけでは心もとありません。
例えば、 @("aaa")と@("bbb")の両方とものUDAを持ってるシンボルだけ取り出したい、とか、@("aaa")と@("bbb")のどちらか一方だけUDAを持ってるシンボルを取り出したい、とか。
こうなるともう、自分でそれ用のテンプレートを作る必要があります。
このとき、ベースはgetSymbolsByUDAの中身を参考にするといいです。これが参考にできた暁には、テンプレートメタマジシャンの称号が獲得できるでしょう。

というか、getSymbolsByUDAの中は相当高度なテンプレートメタ黒魔術が使われているので、D言語の暗黒面を垣間見たって気分になりますね。
isAccessibleMemberでprivateとかアクセスできないのをはじくため、コンパイルできるかどうか確認してみたりなんだり……この挙動の仔細について記すにはこの余白は(ry
ちなみに、この記事の書き始めは上記のようなモノでしたが、今はちょっと変わっています

UDAで取り出したシンボルを活用する

実際のところ、UDAでシンボルを取り出して何をするの?というのが問題です。
今のところ私が観測したとこによると、シリアライザ/デシリアライザの対象を絞り込んだり、カスタマイズするのにつかわれるケースが多いようです。

  • msgpack-d1では、@nonPacked というUDAを使って、シリアライズしないメンバを指定しています。
  • asdf2では@serializationIgnore で無視したり、 @serializedAs でシリアライザのハンドラを外部から指定したりしています。
  • cerealed3では @NoCerealで無視、@Bits!1で配置するビット数の指定などができます。

ここでは、例として以下のように構造体・クラスをUDAを使用してフォーマットする関数を考えてみましょう。

  • クラス・構造体を引数に取るformatDataというテンプレート関数を定義する
  • formatDataはメンバ変数に文字列のUDAがあったら、そのUDAをformatの第一引数にして、内容を文字列化する
  • formatDataは文字列のUDAがついていない変数は文字列化しない

さて、hasUDA, getUDAs, getSymbolsByUDA どのへんが使えるでしょう。

今回は文字列化の対象がUDAがついているものに限られるので、getSymbolsByUDAを使用してメンバのシンボルを列挙しましょう。
列挙されたものはstatic foreachを使って走査的に処理し、getUDAsを用いてUDAを取り出します。
インスタンスのメンバにアクセスするのはちょっと手間が必要で、たとえば変数hogehogeのUDA付きメンバがstatic foreachでsymbolにaliasされる場合、__traits(getMember, hogehoge, __traits(identifier, symbol)) としてアクセスします。(何とかならないのかこれ。)

import std;

struct Data
{
    int    foo;
    string bar;
    @("%.6f") real x;
    @("%.3f") float y;
    @("%06X") uint z;
}

string formatData(T)(T data)
{
    string result;
    static foreach (symbol; getSymbolsByUDA!(T, string)) {
        result ~= format(getUDAs!(symbol, string)[0],
                         __traits(getMember, data, __traits(identifier, symbol)))
               ~ "\n";
    }
    return result;
}

void main()
{
    Data data;
    with (data) {
        foo = 1;
        bar = "hoge";
        x = 10;
        y = 20;
        z = 30;
    }
    writeln(formatData(data));
}

UDAで取り出したシンボルを活用する - 応用編

もう少し高度な例はこちら

  • クラス・構造体を引数に取るformatDataというテンプレート関数を定義する
  • formatDataは、引数のメンバ変数に@formattedWith!"文字列"というテンプレートのインスタンスで指定されたUDAがあったら、そのUDAをformatの第一引数にして、内容を文字列化する
  • formatDataは、引数のメンバ変数に@formattedBy!関数というテンプレートのインスタンスで指定されたUDAがあれば、そのUDAのインスタンス化に使用した関数に変数を渡して文字列を得る。
  • formatDataは、引数のメンバ変数がクラス・構造体なら再帰的に文字列化する
  • formatDataは、上記のUDAがなければ、std.conv.text(member)を使って文字列化する。

とりあえずすぐにわかる問題として、UDAがついていないメンバも出力するため、getSymbolsByUDAは使用不可、というのがありますね。あるんです。__traits(allMembers, T)とかしないといけないので、getSymbolsByUDA参考に新しいテンプレートを作りましょう……
ハードル高くないですか?高いですよね。私も高いと思います。私思うに、このへんPhobosもう少し頑張るべきでは。

さて、このような場合に定石となるのは、以下の3つのテンプレートを作成することです。この定石はasdfで使われています(というか、asdfで使われているものをここでは定石として紹介します)。

  • isXXXX: UDAが特定のパターンにマッチするかどうかを確認します。isFormattedWithなら、formattedWithが文字列でインスタンス化されていることを検査したり、isFormattedByなら、formattedByが文字列を返す関数でインスタンス化されているかってところを検査したりします。
  • hasXXXX: シンボルが特定のUDAを持つかどうかを判定します。hasFormattedWithなら、isFormattedWithにマッチするUDAを持っているかを確認します。
  • getXXXX: シンボルやUDAから使いたい情報を取り出します。getFormattedWithであれば、formattedWithをインスタンス化した文字列を取り出します。

例として、formattedWithに関する上記3つのテンプレートが以下になります。

import std;

struct formattedWith(string str) {}

enum bool isFormattedWith(alias uda) = isInstanceOf!(formattedWith, uda);
enum bool hasFormattedWith(alias symbol) = Filter!(isFormattedWith, __traits(getAttributes, symbol)).length > 0;
template getFormattedWith(alias value)
{
    static if (isFormattedWith!value)
    {
        // UDAから文字列を取り出す
        enum string getFormattedWith = TemplateArgsOf!value[0];
    }
    else
    {
        // シンボルからUDAを取り出す
        alias uda = Filter!(isFormattedWith, __traits(getAttributes, value))[0];
        // UDAから文字列を取り出す
        enum string getFormattedWith = TemplateArgsOf!uda[0];
    }
}

struct Data
{
    int    foo;
    string bar;
    @formattedWith!"%.6f" real x;
    @formattedWith!"%.3f" float y;
    @formattedWith!"%06X" uint z;
}

static assert(isFormattedWith!(formattedWith!"test"));
static assert(getFormattedWith!(formattedWith!"test") == "test");
static assert(!hasFormattedWith!(Data.foo));
static assert( hasFormattedWith!(Data.x));
static assert(getFormattedWith!(Data.x) == "%.6f");

これらを使うには、以下に挙げる例のようにします。基本的には __traits(allMembers, T) ですべてのメンバに対して処置し、 各メンバへのアクセスは __traits(getMember, data, memberName) で行います(プライベートのメンバも取得できるがモジュール外からのアクセスはできない)。
あるいは tupleof (プライベートのメンバも取得できるし、モジュール外からのアクセスもできる)でもOKです。用途によって使い分けます。
以下では使用していませんが、プライベートかどうかは、 __traits(getProtection, xxx) でも取得可能ですので、問題になったら使用を検討しましょう。
is(typeof(xxx))__traits(compiles, xxx)で確認してもいいですが(たぶんコンパイル時間で多少不利になります)。

例は以下。

string formatData(T)(T data)
{
    string result;
    static foreach (memberName; __traits(allMembers, T)) {
        static if (hasFormattedWith!(__traits(getMember, data, memberName))) {
            // formattedWithで変換用の書式があれば
            result ~= format(getFormattedWith!(__traits(getMember, data, memberName)),
                             __traits(getMember, data, memberName))
                   ~ "\n";
        }
    }
    return result;
}

あとは単純にUDAのパターンを増やすだけですね。
最初に挙げた仕様を全部対応させると以下のようになります。

import std;

// formattedWithに関する定義。
struct formattedWith(string str) {}

// isXXXX
enum bool isFormattedWith(alias uda) = isInstanceOf!(formattedWith, uda);
// hasXXXX
enum bool hasFormattedWith(alias symbol) = Filter!(isFormattedWith, __traits(getAttributes, symbol)).length > 0;
// getXXXX
template getFormattedWith(alias value)
{
    static if (isFormattedWith!value)
    {
        // UDAから文字列を取り出す
        enum string getFormattedWith = TemplateArgsOf!value[0];
    }
    else
    {
        // シンボルからUDAを取り出す
        alias uda = Filter!(isFormattedWith, __traits(getAttributes, value))[0];
        // UDAから文字列を取り出す
        enum string getFormattedWith = TemplateArgsOf!uda[0];
    }
}

// formattedByに関する定義。
struct formattedBy(alias func) {}

// isXXXX
enum bool isFormattedBy(alias uda) = isInstanceOf!(formattedBy, uda);
// hasXXXX
enum bool hasFormattedBy(alias symbol) = Filter!(isFormattedBy, __traits(getAttributes, symbol)).length > 0;
// getXXXX
template getFormattedBy(alias value)
{
    static if (isFormattedBy!value)
    {
        // UDAから文字列を取り出す
        alias getFormattedBy = TemplateArgsOf!value[0];
    }
    else
    {
        // シンボルからUDAを取り出す
        alias uda = Filter!(isFormattedBy, __traits(getAttributes, value))[0];
        // UDAから文字列を取り出す
        alias getFormattedBy = TemplateArgsOf!uda[0];
    }
}

struct Hoge
{
    int fuga;
    string piyo;
}

struct Data
{
    static string conv(uint v) { return format("%06d", v); }
    int    foo;
    string bar;
    // 以下のように今回関係のない@(42)は無視されます
    @(42) @formattedWith!"%.6f" real x;
    @formattedWith!"%.3f" float y;
    @formattedBy!conv uint z;
    Hoge hoge;
}

static assert(isFormattedWith!(formattedWith!"test"));
static assert(getFormattedWith!(formattedWith!"test") == "test");
static assert(!hasFormattedWith!(Data.foo));
static assert( hasFormattedWith!(Data.x));
static assert(getFormattedWith!(Data.x) == "%.6f");

static assert(isFormattedBy!(formattedBy!(Data.conv)));
static assert(getFormattedBy!(formattedBy!(Data.conv))(10) == "000010");
static assert(!hasFormattedBy!(Data.foo));
static assert( hasFormattedBy!(Data.z));
static assert(getFormattedBy!(Data.z)(100) == "000100");


string formatData(T)(T data)
{
    string result;
    static foreach (memberName; __traits(allMembers, T)) {
        static if (hasFormattedWith!(__traits(getMember, data, memberName))) {
            // formattedWithで変換用の書式があれば
            result ~= format(getFormattedWith!(__traits(getMember, data, memberName)),
                             __traits(getMember, data, memberName))
                   ~ "\n";
        }
        else static if (hasFormattedBy!(__traits(getMember, data, memberName))) {
            // formattedByで指定された関数を使って変換するなら
            result ~= getFormattedBy!(__traits(getMember, data, memberName))(
                          __traits(getMember, data, memberName))
                   ~ "\n";
        }
        else static if (is(typeof(__traits(getMember, data, memberName)) == struct)
                     || is(typeof(__traits(getMember, data, memberName)) == class)) {
            // 構造体やクラスなら
            result ~= formatData(__traits(getMember, data, memberName));
        }
        else static if (is(typeof(text(__traits(getMember, data, memberName))))) {
            // textで変換可能なら
            result ~= text(__traits(getMember, data, memberName)) ~ "\n";
        }
    }
    return result;
}

void main()
{
    Data data;
    with (data) {
        foo = 1;
        bar = "HOGE";
        x = 10;
        y = 20;
        z = 30;
        hoge.fuga = 1000;
        hoge.piyo = "piyopiyo";
    }
    writeln(formatData(data));
}

  1. MessagePackというバイナリ形式のシリアライズ/デシリアライズを行うライブラリ 

  2. JSON形式のシリアライズ/デシリアライズを行うライブラリ 

  3. 任意(?)のバイナリデータのシリアライズ/デシリアライズを行うライブラリ 

8
0
0

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
8
0