解答11: コンパイル時計算の実践
概要
この解答では、Zigのコンパイル時計算機能を活用して、ルックアップテーブルの生成、ジェネリック型、リフレクション、最適化について学びます。
解答例
Part 1: コンパイル時ルックアップテーブル
const std = @import("std");
// フィボナッチ数列を生成
fn generateFibonacci(comptime n: usize) [n]u64 {
var result: [n]u64 = undefined;
if (n > 0) result[0] = 0;
if (n > 1) result[1] = 1;
var i: usize = 2;
while (i < n) : (i += 1) {
result[i] = result[i - 1] + result[i - 2];
}
return result;
}
// 階乗のテーブルを生成
fn generateFactorials(comptime n: usize) [n]u64 {
var result: [n]u64 = undefined;
var i: usize = 0;
while (i < n) : (i += 1) {
if (i == 0) {
result[i] = 1;
} else {
result[i] = result[i - 1] * i;
}
}
return result;
}
// 素数のリストを生成
fn generatePrimes(comptime max: u32) []const u32 {
comptime {
var primes: [max]u32 = undefined;
var count: usize = 0;
var n: u32 = 2;
while (n <= max) : (n += 1) {
var is_prime = true;
var i: usize = 0;
// 既知の素数で割り切れるかチェック
while (i < count) : (i += 1) {
if (n % primes[i] == 0) {
is_prime = false;
break;
}
// 平方根まででOK
if (primes[i] * primes[i] > n) break;
}
if (is_prime) {
primes[count] = n;
count += 1;
}
}
return primes[0..count];
}
}
// べき乗のテーブルを生成
fn generatePowerTable(comptime base: u32, comptime count: usize) [count]u64 {
var result: [count]u64 = undefined;
var i: usize = 0;
while (i < count) : (i += 1) {
if (i == 0) {
result[i] = 1;
} else {
result[i] = result[i - 1] * base;
}
}
return result;
}
pub fn main() void {
std.debug.print("=== Lookup Tables ===\n\n", .{});
// フィボナッチ数列
comptime var fibs = generateFibonacci(15);
std.debug.print("Fibonacci numbers:\n", .{});
for (fibs, 0..) |fib, i| {
std.debug.print(" F({}) = {}\n", .{i, fib});
}
// 階乗
comptime var facts = generateFactorials(10);
std.debug.print("\nFactorials:\n", .{});
for (facts, 0..) |fact, i| {
std.debug.print(" {}! = {}\n", .{i, fact});
}
// 素数
comptime var primes = generatePrimes(50);
std.debug.print("\nPrimes up to 50:\n ", .{});
for (primes) |prime| {
std.debug.print("{} ", .{prime});
}
std.debug.print("\n", .{});
// べき乗テーブル
comptime var powers = generatePowerTable(2, 16);
std.debug.print("\nPowers of 2:\n", .{});
for (powers, 0..) |power, i| {
std.debug.print(" 2^{} = {}\n", .{i, power});
}
}
Part 2: ジェネリック型生成
const std = @import("std");
// Pair型
fn Pair(comptime T: type, comptime U: type) type {
return struct {
first: T,
second: U,
const Self = @This();
pub fn init(first: T, second: U) Self {
return Self{ .first = first, .second = second };
}
pub fn swap(self: Self) Pair(U, T) {
return Pair(U, T).init(self.second, self.first);
}
pub fn map(
self: Self,
comptime R: type,
comptime S: type,
f: fn(T) R,
g: fn(U) S,
) Pair(R, S) {
return Pair(R, S).init(f(self.first), g(self.second));
}
};
}
// Array型
fn Array(comptime T: type, comptime size: usize) type {
return struct {
data: [size]T,
const Self = @This();
pub fn init(default_value: T) Self {
var result: Self = undefined;
for (&result.data) |*item| {
item.* = default_value;
}
return result;
}
pub fn get(self: Self, index: usize) ?T {
if (index >= size) return null;
return self.data[index];
}
pub fn set(self: *Self, index: usize, value: T) bool {
if (index >= size) return false;
self.data[index] = value;
return true;
}
pub fn map(self: Self, comptime R: type, func: fn(T) R) Array(R, size) {
var result: Array(R, size) = undefined;
for (self.data, 0..) |item, i| {
result.data[i] = func(item);
}
return result;
}
pub fn filter(self: Self, predicate: fn(T) bool) []const T {
comptime var temp: [size]T = undefined;
var count: usize = 0;
for (self.data) |item| {
if (predicate(item)) {
temp[count] = item;
count += 1;
}
}
return temp[0..count];
}
};
}
// Option型
fn Option(comptime T: type) type {
return union(enum) {
Some: T,
None,
const Self = @This();
pub fn isSome(self: Self) bool {
return switch (self) {
.Some => true,
.None => false,
};
}
pub fn isNone(self: Self) bool {
return !self.isSome();
}
pub fn unwrap(self: Self) T {
return switch (self) {
.Some => |val| val,
.None => @panic("unwrap on None"),
};
}
pub fn unwrapOr(self: Self, default: T) T {
return switch (self) {
.Some => |val| val,
.None => default,
};
}
pub fn map(self: Self, comptime R: type, func: fn(T) R) Option(R) {
return switch (self) {
.Some => |val| Option(R){ .Some = func(val) },
.None => Option(R).None,
};
}
pub fn andThen(self: Self, comptime R: type, func: fn(T) Option(R)) Option(R) {
return switch (self) {
.Some => |val| func(val),
.None => Option(R).None,
};
}
};
}
pub fn main() void {
std.debug.print("=== Generic Types ===\n\n", .{});
// Pair
const pair = Pair(i32, []const u8).init(42, "answer");
std.debug.print("Pair: ({}, {s})\n", .{pair.first, pair.second});
const swapped = pair.swap();
std.debug.print("Swapped: ({s}, {})\n", .{swapped.first, swapped.second});
// Array
var arr = Array(i32, 5).init(0);
_ = arr.set(0, 10);
_ = arr.set(1, 20);
_ = arr.set(2, 30);
std.debug.print("\nArray[1]: {?}\n", .{arr.get(1)});
// Option
const some = Option(i32){ .Some = 42 };
const none = Option(i32).None;
std.debug.print("\nOption some: {}\n", .{some.unwrapOr(0)});
std.debug.print("Option none: {}\n", .{none.unwrapOr(0)});
std.debug.print("Is some: {}\n", .{some.isSome()});
std.debug.print("Is none: {}\n", .{none.isNone()});
}
Part 3: コンパイル時リフレクション
const std = @import("std");
const Person = struct {
name: []const u8,
age: u32,
email: []const u8,
active: bool,
};
// 構造体のフィールド数を取得
fn fieldCount(comptime T: type) usize {
return std.meta.fields(T).len;
}
// 構造体のフィールド名を配列で取得
fn fieldNames(comptime T: type) []const []const u8 {
comptime {
const fields = std.meta.fields(T);
var names: [fields.len][]const u8 = undefined;
for (fields, 0..) |field, i| {
names[i] = field.name;
}
return &names;
}
}
// 構造体のフィールド型を配列で取得
fn fieldTypes(comptime T: type) []const type {
comptime {
const fields = std.meta.fields(T);
var types: [fields.len]type = undefined;
for (fields, 0..) |field, i| {
types[i] = field.type;
}
return &types;
}
}
// 特定のフィールドが存在するか確認
fn hasField(comptime T: type, comptime field_name: []const u8) bool {
const fields = std.meta.fields(T);
inline for (fields) |field| {
if (std.mem.eql(u8, field.name, field_name)) {
return true;
}
}
return false;
}
// 特定の型のフィールドを持つか確認
fn hasFieldOfType(comptime T: type, comptime FieldType: type) bool {
const fields = std.meta.fields(T);
inline for (fields) |field| {
if (field.type == FieldType) {
return true;
}
}
return false;
}
// 構造体の情報を表示
fn printStructInfo(comptime T: type) void {
std.debug.print("Struct: {}\n", .{T});
std.debug.print("Size: {} bytes\n", .{@sizeOf(T)});
std.debug.print("Alignment: {} bytes\n", .{@alignOf(T)});
std.debug.print("Fields: {}\n", .{fieldCount(T)});
const fields = std.meta.fields(T);
inline for (fields) |field| {
std.debug.print(" - {s}: {} ({} bytes)\n",
.{field.name, field.type, @sizeOf(field.type)});
}
}
// 列挙型の値を配列で取得
fn enumValues(comptime T: type) []const T {
comptime {
const info = @typeInfo(T);
if (info != .Enum) {
@compileError("enumValues requires enum type");
}
var values: [info.Enum.fields.len]T = undefined;
for (info.Enum.fields, 0..) |field, i| {
values[i] = @enumFromInt(field.value);
}
return &values;
}
}
pub fn main() void {
std.debug.print("=== Reflection ===\n\n", .{});
printStructInfo(Person);
std.debug.print("\nField count: {}\n", .{fieldCount(Person)});
std.debug.print("Has 'name' field: {}\n", .{hasField(Person, "name")});
std.debug.print("Has 'address' field: {}\n", .{hasField(Person, "address")});
std.debug.print("Has u32 field: {}\n", .{hasFieldOfType(Person, u32)});
std.debug.print("Has f64 field: {}\n", .{hasFieldOfType(Person, f64)});
}
Part 4: コンパイル時最適化
const std = @import("std");
// CRC32テーブルを生成
fn generateCRC32Table() [256]u32 {
comptime {
var table: [256]u32 = undefined;
var i: usize = 0;
while (i < 256) : (i += 1) {
var crc: u32 = @intCast(i);
var j: usize = 0;
while (j < 8) : (j += 1) {
if (crc & 1 == 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
table[i] = crc;
}
return table;
}
}
const crc32_table = generateCRC32Table();
// CRC32を計算
fn crc32(data: []const u8) u32 {
var crc: u32 = 0xFFFFFFFF;
for (data) |byte| {
const index = @as(u8, @truncate((crc ^ byte) & 0xFF));
crc = (crc >> 8) ^ crc32_table[index];
}
return ~crc;
}
// ビット反転テーブルを生成
fn generateBitReverseTable() [256]u8 {
comptime {
var table: [256]u8 = undefined;
var i: usize = 0;
while (i < 256) : (i += 1) {
var byte: u8 = @intCast(i);
var reversed: u8 = 0;
var j: u8 = 0;
while (j < 8) : (j += 1) {
reversed = (reversed << 1) | (byte & 1);
byte >>= 1;
}
table[i] = reversed;
}
return table;
}
}
const bit_reverse_table = generateBitReverseTable();
// ビットを反転
fn reverseBits(byte: u8) u8 {
return bit_reverse_table[byte];
}
// ループ展開を使った最適化
fn sumArrayUnrolled(comptime size: usize, array: [size]i32) i32 {
var sum: i32 = 0;
inline for (array) |value| {
sum += value;
}
return sum;
}
// コンパイル時分岐による最適化
fn optimizedMultiply(comptime factor: i32, value: i32) i32 {
if (comptime std.math.isPowerOfTwo(@abs(factor))) {
const shift = comptime std.math.log2_int(u32, @abs(factor));
const result = value << shift;
return if (factor < 0) -result else result;
} else {
return value * factor;
}
}
pub fn main() void {
std.debug.print("=== Compile-time Optimization ===\n\n", .{});
// CRC32
const data = "Hello, World!";
const checksum = crc32(data);
std.debug.print("CRC32 of '{s}': 0x{x:0>8}\n\n", .{data, checksum});
// ビット反転
const original: u8 = 0b10110100;
const reversed = reverseBits(original);
std.debug.print("Original: 0b{b:0>8}\n", .{original});
std.debug.print("Reversed: 0b{b:0>8}\n\n", .{reversed});
// ループ展開
const numbers = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 };
const sum = sumArrayUnrolled(8, numbers);
std.debug.print("Sum (unrolled): {}\n\n", .{sum});
// 最適化された乗算
const result1 = optimizedMultiply(8, 10);
const result2 = optimizedMultiply(7, 10);
std.debug.print("10 * 8 (shift): {}\n", .{result1});
std.debug.print("10 * 7 (multiply): {}\n", .{result2});
}
ポイント解説
1. comptime変数とブロック
comptimeブロックを使うことで、コンパイル時に実行されるコードを明示的に記述できます:
comptime {
// このコード全体がコンパイル時に実行される
var result = calculateSomething();
}
2. ジェネリック型の実装
型を返す関数を使って、ジェネリック型を実装します:
fn GenericType(comptime T: type) type {
return struct {
value: T,
// ...
};
}
3. リフレクションの活用
std.metaモジュールを使って型情報を取得します:
const fields = std.meta.fields(T);
inline for (fields) |field| {
// フィールド情報を処理
}
4. コンパイル時最適化
ルックアップテーブルをコンパイル時に生成することで、実行時のパフォーマンスを向上させます。
よくある間違い
1. comptime変数の変更
// 間違い
fn example() void {
comptime var x = 10;
x += 1; // エラー: comptime変数は変更できない(関数スコープ外で)
}
// 正しい
fn example() void {
comptime {
var x = 10;
x += 1; // OK: comptimeブロック内なら変更可能
}
}
2. 型の比較
// 間違い
if (T == i32) { ... } // エラー: 型の比較は==ではできない
// 正しい
if (T == i32) { ... } // 実は正しい(Zigでは型も値)
// または
if (@typeInfo(T) == .Int) { ... }
3. inline forの誤用
// 間違い
inline for (runtime_array) |item| { ... } // エラー: ランタイム配列には使えない
// 正しい
inline for (comptime_array) |item| { ... } // OK: コンパイル時配列
発展課題
Challenge 1: コンパイル時正規表現
コンパイル時に正規表現をパースして、効率的なマッチャーを生成してください。
Challenge 2: 型レベルのリスト操作
型のリストを操作する関数(map、filter、reduceなど)を実装してください。
Challenge 3: コンパイル時SQLクエリ検証
SQL文字列をコンパイル時にパースして、構文エラーを検出してください。
Challenge 4: 自動シリアライザー生成
構造体の型情報から、自動的にシリアライズ/デシリアライズ関数を生成してください。
Challenge 5: ゼロコスト抽象化
仮想関数テーブルを使わずに、コンパイル時にポリモーフィズムを実現してください。
まとめ
この課題を通じて、以下を学びました:
- comptime変数と関数: コンパイル時計算の基礎
- 型の生成: ジェネリックプログラミング
- リフレクション: 型情報の取得と活用
- 最適化: コンパイル時計算によるパフォーマンス向上
Zigのコンパイル時計算は、C++のテンプレートやRustのマクロよりも直感的で強力な機能を提供します。これらの機能を活用することで、ゼロコスト抽象化を実現できます。