解答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のマクロよりも直感的で強力な機能を提供します。これらの機能を活用することで、ゼロコスト抽象化を実現できます。