Zigで実行時に指定された文字列のフィールドの値を取得する方法

`Zig`は`RTTI`(RunTime Type Information)を備えていないため、無理です。あきらめましょう。

```

Zigで実行時に指定された文字列のフィールドの値を取得する方法



                                            終　　
                                         制作・著作
                                          ━━━━━
                                          ⓀⓉⓏ　
```

## いやそれでもなんとかしたいっす

妥協案として、指定された文字列に対して、フィールドにアクセスする関数を対応づけたルックアップテーブルを引くという方法が挙げられる。

このようなルックアップテーブルをコンパイル時に作成する手段として、標準ライブラリで`std.StaticStringMap`が提供されている[^1].
[^1]: zig 0.12.0までは、ComptimeStringMapだったが、0.13.0でリネームされた

この際、キーとなる文字列は、フィールド名と一致するようコンパイル時に検証されていると安心である。

ということで、以下のような型を作成した。

:::note warn
zig 0.14のnightlyで確認しています。
zigは、しばしば破壊的変更が行われてれいるため、将来のバージョンでは使えないかもしれないことにご注意を
:::

```zig
fn BindMap(comptime InputType: type) type {
    return struct {
        pub const Fn = *const fn (in: InputType, out: *std.ArrayList([]const u8)) anyerror!void;
        pub const KV = struct {std.meta.FieldEnum(InputType), Fn};
        
        pub fn init(comptime kvs: []const KV) std.StaticStringMap(Fn) {
            const fields = std.meta.fields(InputType);
            comptime std.debug.assert(kvs.len == fields.len);

            comptime var map_kvs: [kvs.len](struct []const u8, Fn}) = undefined;
            for (kvs, 0..) |kv, i| {
                map_kvs[i] = .{ @tagName(kv[0]), kv[1] };
            }

            return std.StaticStringMap(Fn).initComptime(map_kvs);
        }
    };
}
```

これは、文字列で指定されたフィールドの値を`std.ArrayList`に放り込むことを目的としたものである。

ポイントは、

* `init`関数でルックアップテーブルのキーとする型を`std.meta.FieldEnum`にし、フィールド名として存在しない名前で構成した場合はコンパイルエラーにさせる
* `kvs`の要素数と、型のフィールドの要素数をコンパイル時に要請する
    * 一部のフィールドだけを対象とするのであれば不要

また、`StaticStringMap.init`に渡す引数の型は`anytype`となっているが、`len`で要素数が取れ、`[]`で要素アクセスできればよく、実は配列やスライスを渡しても構わなかったりする。

### 使い方

以下の例をご参照あれ

```zig
const std = @import("std");

// BindMapは上記の定義をそのまま使う

const Foo = struct {
  a: []const u8,
  b: []const u8,
  c: []const u8,
};

const Map = BindMap(Foo).init(&.{
    .{.a, Binder.getA},
    .{.b, Binder.getB},
    .{.c, Binder.getC},
});

const Binder = struct {
    fn getA(in: Foo, out: *std.ArrayList([]const u8)) !void {
        try out.append(in.a);
    }
    fn getB(in: Foo, out: *std.ArrayList([]const u8)) !void {
        try out.append(in.b);
    }
    fn getC(in: Foo, out: *std.ArrayList([]const u8)) !void {
        try out.append(in.c);
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        _ = gpa.deinit();
    }
    const allocator = gpa.allocator();

    const foo: Foo = .{
        .a = "hoge",
        .b = "uge",
        .c = "moge",
    };
    
    const fields: []const []const u8 = &.{"a", "c"};

    var out = std.ArrayList([]const u8).init(allocator);
    defer out.deinit();

    for (fields) |field| {
        const binder = Map.get(field);
        std.debug.assert(binder != null);
        try binder.?(foo, &out);
    }
}
```

### 実例

あらかじめ`zig-clap`[^2] を使用して構成した引数情報のうち、一部を起動する子プロセスに渡したく模索して妥協案として行きつきました。
[^2]: https://github.com/Hejsil/zig-clap

```zig
const Symbol = []const u8;
const FilePath = []const u8;

const GenerateSetting = struct {
    source_dir_path: []FilePath,
    output_dir_path: FilePath,
    watch: bool,
};

const Binder = struct {
    // 設定を保持する型にclapで使用するenumを持たせている
    const ArgId = GenerateSetting.ArgId(.{});
    // そのenumにclapのパーザに渡すclap.Param(ArgId)のスライスを持たせている
    const decls = ArgId.Decls;
    
    fn bindSourceDir(setting: GenerateSetting, args: *std.ArrayList(Symbol)) !void {
        const decl = comptime findDecl(ArgId, decls, .source_dir_path);
        try args.append("--" ++ decl.names.long.?);

        for (setting.source_dir_path) |path| {
            try args.append(path);
        }
    }
    fn bindOutputDir(setting: GenerateSetting, args: *std.ArrayList(Symbol)) !void {
        const decl = comptime findDecl(ArgId, decls, .output_dir_path);
        try args.append("--" ++ decl.names.long.?);

        try args.append(setting.output_dir_path);
    }
    fn bindWatchMode(setting: GenerateSetting, args: *std.ArrayList(Symbol)) !void {
        if (setting.watch) {
            const decl = comptime findDecl(ArgId, decls, .watch);
            try args.append("--" ++ decl.names.long.?);
        }
    }
};

fn findDecl(comptime Id: type, comptime decls: []const clap.Param(Id), comptime id: Id) clap.Param(Id) {
    inline for (decls) |decl| {
        if (decl.id == id) {
            std.debug.assert(decl.names.long != null);
            return decl;
        }
    }

    @compileError(std.fmt.comptimePrint("Not contained CLI arg setting: {}",.{id}));
}

const GenerateConfigMap = 
    ConfigBindMap(GenerateSetting).init(&.{
        .{.source_dir_path, Binder.bindSourceDir},
        .{.output_dir_path, Binder.bindOutputDir},
        .{.watch, Binder.bindWatchMode},
    })
;
```