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

## はじめに

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

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

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

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

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

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

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

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

う〜ん。いけてない[^1]。

[^1]:個人的感想です。

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

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

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

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

例えばこんな感じ

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

void foo(Opt options) { ... }
```

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

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

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

```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);
```

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

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

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

```zig
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.enums`に`EnumFieldStruct`なるものが定義されている。

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

使い方は以下のよう

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

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

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

使う際は、以下のよう

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

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

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

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

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

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

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

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

使い方は以下の通り

```zig
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`に記述されたテストコードを見るとよいでしょう。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```zig
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`をカスタマイズすることができる。

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

```zig
//
// インデックス
//
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`が強力。