1
0

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.

Zig言語のオプションセット詰め合わせ

Posted at

はじめに

zig言語の標準ライブラリを散策していたら、種々のオプションセット表現が見つかったので、事例を添えて解説していく。

ケース1: 複数のオプションを受け付ける関数

今、複数のオプションを受け付ける関数を書きたいとする。
さてどう書こうか?

オプションの数だけbooleanの引数を用意する、と言うのがパッと思いつく。

例えば3つのオプションを受け付ける関数であれば、以下のようになるだろう。

fn foo(opt1: bool, opt2: bool, opt3: bool) void { ... }

そして以下のようにして使う。

pub fn main() void {
    foo(true, false, true);
}

う〜ん。いけてない1

呼び出し側から見て、どのオプションを有効にしたか全くわからないところが特に。
また、オプションを追加した際は、引数を追加して回らないといけないのもダルい。

もっといい方法はないものか。

解決案1: enumビットフラグ

C言語界隈ではビットフラグなenumがよく用いられる。

例えばこんな感じ

enum Opt { 
   Opt1 = 1 << 0,
   Opt2 = 1 << 1,
   Opt3 = 1 << 2
};

void foo(Opt options) { ... }

使う際は、以下のようにビット演算して渡す。

int main() {
    foo(Opt1 | Opt3)
    return 0;
}

zigでこれが使えないだろうか?
試してみる。

const Options = enum(u32) {
   Opt1 = 1 << 0,
   Opt2 = 1 << 1,
   Opt3 = 1 << 2
};

fn foo(opts: Options) void { ... }

pub fn main() void {
    foo(Options.Opt1 | Options.Opt3);
}

残念、コンパイルエラーになった。

error: invalid operands to binary bitwise expression: 'Enum' and 'Enum'
    foo(Options.Opt1 | Options.Opt3);

原因は、メッセージの通りenumbit orをサポートしていないから。

少し妥協を許すのであれば、

関数の定義を以下のようにすることで一応コンパイルは通る。

fn foo(opts: u32) void {
  std.debug.print("opt2: {}\n", .{ opts & @enumToInt(Options.Opt2) != 0 });
}

fn main() void {
    foo(@enumToInt(Options.Opt1) | @enumToInt(Options.Opt3));
}

ただ@enumToIntをつけて回らないといけないことと、ビットフラグなenumという表現がロストしているのがツラい。

もっと良い方法はないものかと、ライブラリの森をさまよっていたところ、いいものを見つけた。

解決策2: EnumFieldStructを使う

名前空間std.enumsEnumFieldStructなるものが定義されている。

これは、enumから任意の型なフィールドセットをもつ構造体を作成してくれる関数である。
フィールドの型をboolにすれば擬似的なオプションセットが作れるという算段である。

使い方は以下のよう

const OptionSet = std.enum.EnumFieldStruct(Options, bool, false);

作成される型は以下と同等の定義となっている

const OptionSet = struct {
    Opt1: bool = false,
    Opt2: bool = false,
    Opt3: bool = false,
};

使う際は、以下のよう

fn foo(opts: OptionSet) void {
    std.debug.print("opt2: {}\n", .{ opts.Opt2 });
}

pub fn main() void {
    foo(.{ .Opt1 = true, .Opt3 = true });
}

型の情報を残しつつ、使う側からもどのオプションを有効にしたかが一目瞭然でわかる。

この形、Javascriptでのオブジェクトリテラルを使ったオプションセット表現2としてよく見かける気がする。

foo({ opt1: true, opt3: true });

ケース2: enumの値を集合として使う

std.enums名前空間には他にも型が用意されており、EnumSetはenum値を集合として扱うことができる。
この型は、std.EnumSetとしてリマップされている。

使い方は以下の通り

const OptionSet = std.EnumSet(Options);

pub fn main() void {
    // 空の集合を作る
    var opts = OptionSet.initEmpty();
    opts.insert(.Opt1);
    opts.insert(.Opt3);

    std.debug.print("{}\n", .{ opts.contains(.Opt2) });
}

この型は、内部的にbit maskで管理されているため、メモリ使用量も抑えられる。

より詳細な例は、標準ライブラリstd/enums.zigに記述されたテストコードを見るとよいでしょう。

EnumSetは、元のenumの値は使用せず、enumの値を昇順にソートし、配列のインデックスとenumの値の名前を関連づけていることに注意。
ケース1Optionsの場合であれば擬似的に

const Options = enum {
    Opt1 = 0,
    Opt2 = 1,
    Opt3 = 2,
}

とされていることと同等になる。

内部のビットマスクの値は以下のようにして取り出すことができる。

var opts = OptionSet.initFull();
_ = opts.bits.mask

FFIでビットマスクの値を組み立てる際にEnumSetを使うと便利。3

ケース3: enumの値が飛び飛びでもビットマスクを取り出したい

前節でEnumSetを解説した。
enumの値が振られていない、または0からの連番であれば、enumの値のbit orとビットマスクの値が一致する。

しかし以下のように値に飛び番がある場合、enumの値のbit orとビットマスクの値が一致しなくなる。

const Alphabet = enum {
   A = 1 << 0,
   B = 1 << 1,
   D = 1 << 3,
};

このケースでは、EnumSetを使うことはできない。

しかし、EnumSetの実態は、std/enums.zigに記述されており、

EnumSet = IndexedSet(EnumIndexer(E), mixin.EnumSetExt);

となっていることがわかる。

ここで、IndexedSetの第一引数はEnumSetのキーの内部表現、第二引数は拡張メソッドである。
ここで重要なのはキーの内部表現で、そのインターフェースは

struct {
    // enumの型
    pub const Key: type; 
    // 要素数
    pub const count: usize; 
    // enumの値からインデックスを求める
    pub fn indexOf(e: E) usize; 
    // 指定されたインデックスのenumの値を求める
    pub fn keyForIndex(i: usize) E; 

である。

この型に適合するようにキーの内部表現を用意することで、EnumSetをカスタマイズすることができる。

とりまこんなの書いてみた

//
// インデックス
//
pub fn SparseEnumIndexer(comptime E: type) type {
    const S = struct {
        fn maxLength() comptime_int {
            var result: i32 = -1;

            inline for(std.meta.fields(E)) |f| {
                var v: i32 = f.value;
                var c: comptime_int = @popCount(v);
                if (c != 1) {
                    @compileError("enum value is not bit flag");
                }

                result = @max(result, @ctz(v));
            }

            return result + 1;
        }

        fn toArray(comptime len: comptime_int) [len]E {
            var result: [len]E = undefined;

            inline for (std.meta.fields(E)) |f| {
                var v: i32 = f.value;
                var i = @ctz(v);

                result[i] = @field(E, f.name);
            }

            return result;
        }
    };  

    const len = S.maxLength();
    const keys = S.toArray(len);
  
    return struct {
        pub const Key = E; 
        pub const count: usize = len;

        pub fn indexOf(e: E) usize {
            return @ctz(@enumToInt(e));
        }

        pub fn keyForIndex(i: usize) E {
            return keys[i];
        }
    };
}

//
// 拡張メソッド
//
pub fn SparseEnumSet(comptime Self: type) type {
    return struct {
        pub fn init(args: anytype) Self {
            var self = Self {};

            self.includes(args);

            return self;
        }

        pub fn includes(self: *Self, args: anytype) void {
            const typ = @typeInfo(@TypeOf(args)).Struct;
            if (! typ.is_tuple) {
                @compileError("need pass by tuple\n");
            }

            const len = typ.fields.len;
            comptime var i = 0;

            inline while (i < len) : (i += 1) {
                self.insert(args[i]);
            }
        }

        pub fn excludes(self: *Self, args: anytype) void {
            const typ = @typeInfo(@TypeOf(args)).Struct;
            if (! typ.is_tuple) {
                @compileError("need pass by tuple\n");
            }

            const len = typ.fields.len;
            comptime var i = 0;

            inline while (i < len) : (i += 1) {
                self.remove(args[i]);
            }
        }
    };
}

const Options = enum(u32) {
   Opt1 = 1 << 0,
   Opt2 = 1 << 1,
   Opt3 = 1 << 2,
   Opt55 = 1 << 10,
   Opt99 = 1 << 15,
};

const SparseOptions = std.enums.IndexedSet(SparseEnumIndexer(Options), SparseEnumSet);

test "testing for sparse enum set" {
    var opts = SparseOptions.init(.{ .Opt1 });

    try std.testing.expectEqual(true, opts.contains(.Opt1));
    try std.testing.expectEqual(false, opts.contains(.Opt2));
    try std.testing.expectEqual(false, opts.contains(.Opt3));
    try std.testing.expectEqual(false, opts.contains(.Opt55));

    try std.testing.expect(opts.bits.mask == 1);

    opts.includes(.{ .Opt3, .Opt55 });

    try std.testing.expectEqual(true, opts.contains(.Opt1));
    try std.testing.expectEqual(false, opts.contains(.Opt2));
    try std.testing.expectEqual(true, opts.contains(.Opt3));
    try std.testing.expectEqual(true, opts.contains(.Opt55));
    try std.testing.expect(opts.bits.mask == 1029);

    opts.excludes(.{ .Opt1, .Opt2, .Opt3 });

    try std.testing.expectEqual(false, opts.contains(.Opt1));
    try std.testing.expectEqual(false, opts.contains(.Opt2));
    try std.testing.expectEqual(false, opts.contains(.Opt3));
    try std.testing.expectEqual(true, opts.contains(.Opt55));
    try std.testing.expect(opts.bits.mask == 1024);
}

上手くカスタマイズできた感じかな?

まとめ

Zig言語のenumはC言語のような手軽さはない一方で、ライブラリによる手厚い保護でより使い勝手が良くなってるように思えました。

あとZig言語のcomptimeが強力。

  1. 個人的感想です。

  2. もしくはpythonで最後の引数にオプションセットとしてdictを渡すみたいな

  3. 取り出した瞬間、enum flagであった情報はロストするけれども

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?