Zigの@typeInfoと@Typeを理解する
最初に結論
-
@TypeOf()
: 値 → 型 (型名) -
@typeInfo()
: 型 (型名) → 型情報 -
@Type()
: 型情報 → 型 -
anytype
: どんな型の値でも受け取れる、という意味の型 -
type
: 型 (型名) を受け取る場合や、関数で型を返す場合に使う、型
(ちなみに@
はZigの組み込み関数を表す。)
まえがき: メタプログラミングだけど普通のコード
Zigはcomptime
という強力なメタプログラミングの仕組みを持っているのだけれど、C++やRustやNimなどと違って、マクロやメタプログラミング専用の記法は基本的に持っていない。
ではどうやってメタプログラミングするのかというと、comptime
は単に実行時にできることをコンパイル時にできるようにするというもので、さらにZigの画期的なアイデアとして、型そのものの情報をシンプルに構造体として読み取ったり操作することができる。
comptime
は、極端にいえばLispみたいなもの[1]だと思ってもらえれば良いと思うのだけれど、構文木 (AST) の操作ができるわけではないところに、Zigの美学[2]がある。
実行時にできることの全てがコンパイル時にできるとは限らないのだけれど、この考え方に慣れると、今までのマクロやテンプレートが面白いようにシンプルに普通のコードで書かれていくので、まるでLispのような面白さ[1:1]がある。
@typeInfoで型情報を取得する
次のようなコードを実際に実行してみる。
const std = @import("std");
pub fn main() !void {
const v = [_]u8{1,2,3};
const t = @TypeOf(v); // 型を取得する
const typeInfo = @typeInfo(t); // 型情報を取得する
std.debug.print("{any} || {any}", .{t, typeInfo});
}
結果は [3]u8 || builtin.Type{ .Array = builtin.Type.Array{ .len = 3, .child = u8, .sentinel = null } }
と表示される。[3]
後半を見やすいように整理すると、次のようになる。
builtin.Type {
.Array = builtin.Type.Array {
.len = 3,
.child = u8,
.sentinel = null
}
}
ここで、buildin.Type
というのはunion(enum)
(Tagged Union) で、builtin.Type.Array
はstruct
なのだけれど、細かな定義は一旦置いておいて、実際に構造体として取得できることが重要。
元にした値は [_]u8{1,2,3}
で、型は[3]u8
だったので、それがそのまま情報として構造体を取得できているのがわかる。(ちなみにsentinelというのは配列の最後が何かを示す、Zig独自の概念で、これはこれで便利だけれど今回は割愛。)
ここで取得した typeInfo
は、@Type
で型に戻すことができる。つまり、恣意的に操作を行った独自の型を作ることができる。以下はその例[4]。
const myType = std.builtin.Type{
.Struct = .{
.is_tuple = false,
.decls = &.{},
.fields = &.{},
.layout = .Auto,
},
};
const MyType = @Type(myType);
const myInstance = MyType{}
// 上記は https://zenn.dev/ousttrue/books/b2ec4e93bdc5c4/viewer/4383ac より引用・修正
これらを利用してどんなことができるかというと、例えばanytype
と組み合わせてC++のテンプレート (総称型) のような処理が可能で、具体例としては下記記事のエラー付加情報 (Error Payload) のような処理に活用することも可能。
記事に書かれているコードを一部引用・抜粋して紹介すると、
const enum_info = std.builtin.Type.Enum {
.layout = .Auto,
.tag_type = u16,
.fields = &enum_fields,
.decls = &[0]std.builtin.Type.Declaration{},
.is_exhaustive = true,
};
const ErrorEnums = @Type(.{.Enum = enum_info});
といった感じで使われている。@Type()
の中身がstd.builtin.Type {}
でなくて.{}
なのは、型推論によるものなので、構造はさっきのコードと変わらない。(.{}
はJavaScriptのオブジェクトのようなもので、無名関数ならぬ無名構造体。)
ちなみに、std.builtin.Type
の定義はunion(enum)
になっていると紹介したけれど、.Enum
であったり.Array
である場合に構造体が持つ情報が違っていて、これがZigのunion
(Tagged Union) の特徴であり、慣れると便利[5]。
また、このコードが書かれている関数の定義は以下の様になっていて、第一引数で型名 (type
) を受け取って、内部で操作した新しい型定義 (type
) を返すようになっている。
// 関数定義
fn ErrorPayloadFor(comptime ErrorSet: type, comptime types: anytype) type
// 使い方
const CustomErrorPayload = ErrorPayloadFor(CustomError, .{
.number_error = struct {
number: u64,
},
.other_error = struct {
message: []const u8,
},
});
まとめ: シンプルさの恩恵
ということで改めてまとめると、最初に紹介したようになる。
-
@TypeOf()
: 値 → 型 (型名) -
@typeInfo()
: 型 (型名) → 型情報 -
@Type()
: 型情報 → 型 -
anytype
: どんな型の値でも受け取れる、という意味の型 -
type
: 型 (型名) を受け取る場合や、関数で型を返す場合に使う、型
C++のテンプレートなどと違って、コードをパッと見たときに何の型が返るのか分かりづらい[6]というのが欠点ではあるものの、シンプルに普通のコードで書かれているというのは、マクロやテンプレート専用の構文を覚えなくて済むし、普通のコードとほぼ変わらず読み書きすることができるので、開発のハードルもコードリーディングのコストも下がると思う。
Zigのこうしたシンプルさや読みやすさに対する貪欲さは他の言語に比べて異常だと自分は感じていて、アロケータも含めて、コードに書かれていない隠された挙動がない[2:1]、という部分も含めて、Zigのシンプルさというのは本当に洗練されていると思う[7]。
-
Lispではコード自体をデータとして扱うことが可能で、データとコードの境目が曖昧というか境がない。Lispが括弧だらけなのはこのため。参考: https://postd.cc/why-lisp/ ↩︎ ↩︎
-
https://ziglang.org/ No hidden control flow. No hidden memory allocations. No preprocessor, no macros. (隠された制御フローがない、隠されたメモリ割当がない、プリプロセッサやマクロがない。) ↩︎ ↩︎
-
【6/11追記】この方法だと、
print
できる型情報が実は限られるので、@compileLog()
を使ってデバッグをするのがおすすめ: https://zenn.dev/link/comments/91341fe7f952ff ↩︎ -
コードは https://zenn.dev/ousttrue/books/b2ec4e93bdc5c4/viewer/4383ac より引用して一部修正 ↩︎
-
std.builtin.Type
の定義自体もZigで書かれていて、読み解くのはそれほど難しくない。 https://github.com/ziglang/zig/blob/629f0d23b5c0768b5957688591f6fa6216ae4dd3/lib/std/builtin.zig#L228 ↩︎ -
型推論である程度の型は定まるはずなので、LSP、Language Server Protocolやエディタが進化すれば将来的にはさほど問題にはならなそう。それにC++のdecltypeなんかも読解は大変だったりするので、慣れれば同じかもしれない。 ↩︎
-
少し蛇足だけれど、メタプログラミングを許容しながらも、演算子のオーバーロードがない、構文木(AST)操作がないというのは、LispやC++において、メタプログラミングによる独自のコードにより可読性が下がることの反省が活かされている。こうした部分も含めてZigのコードというのはシンプルで読みやすい。 ↩︎
Discussion