はじめに
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);
原因は、メッセージの通りenum
がbit 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.enums
にEnumFieldStruct
なるものが定義されている。
これは、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
の値の名前を関連づけていることに注意。
ケース1
のOptions
の場合であれば擬似的に
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
が強力。