Zig
はRTTI
(RunTime Type Information)を備えていないため、無理です。あきらめましょう。
Zigで実行時に指定された文字列のフィールドの値を取得する方法
終
制作・著作
━━━━━
ⓀⓉⓏ
いやそれでもなんとかしたいっす
妥協案として、指定された文字列に対して、フィールドにアクセスする関数を対応づけたルックアップテーブルを引くという方法が挙げられる。
このようなルックアップテーブルをコンパイル時に作成する手段として、標準ライブラリでstd.StaticStringMap
が提供されている1.
この際、キーとなる文字列は、フィールド名と一致するようコンパイル時に検証されていると安心である。
ということで、以下のような型を作成した。
zig 0.14のnightlyで確認しています。
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
で要素数が取れ、[]
で要素アクセスできればよく、実は配列やスライスを渡しても構わなかったりする。
使い方
以下の例をご参照あれ
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 を使用して構成した引数情報のうち、一部を起動する子プロセスに渡したく模索して妥協案として行きつきました。
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},
})
;
-
zig 0.12.0までは、ComptimeStringMapだったが、0.13.0でリネームされた ↩