Open37

Zigを学ぶ

mena1mena1

Zigに興味を持ったので学んでみる
Zigが作られた背景や言語仕様・RustとZigの比較などを学んでいきたい

mena1mena1

まずは環境構築から

brew install zig

Neovimで開発を行うためZigのlspを入れる
自分はmasonで行った

:MasonInstall zls
local nvim_lsp = pcall(require, 'lspconfig')

nvim_lsp.zls.setup {
  -- format
  on_attach = on_attach,
  cmd = { "zls" },
  filetypes = { "zig" },
}
mena1mena1

zigのポリシー

  • Communicate intent precisely.
  • Edge cases matter.
  • Favor reading code over writing code.
  • Only one obvious way to do things.
  • Runtime crashes are better than bugs.
  • Compile errors are better than runtime crashes.
  • Incremental improvements.
  • Avoid local maximums.
  • Reduce the amount one must remember.
  • Focus on code rather than style.
  • Resource allocation may fail; resource deallocation must succeed.
  • Memory is a resource.
  • Together we serve the users.
mena1mena1

シンプルかつ基本的に標準で備わっているのは◯

C++、Rust、D には非常に多くの機能があるため、作業中のアプリケーションの実際の意味から注意が逸れてしまう可能性があります。アプリケーション自体をデバッグするのではなく、プログラミング言語の知識をデバッグしている自分に気付くことがあります。Zig にはマクロはありませんが、それでも複雑なプログラムを明確かつ反復のない方法で表現できるほど強力です。Rust にも のような特殊なケースのマクロがありformat!、これはコンパイラ自体に実装されています。一方、Zig では同等の関数が標準ライブラリに実装されており、コンパイラに特殊なケースのコードはありません。

https://ziglang.org/learn/why_zig_rust_d_cpp/

mena1mena1

ZigはRustのように複雑な処理は抽象化したり、Go のようにGo generateで複雑な処理を隠したりなどしないから、挙動が追いやすいと..

魔法はない。冒頭の3つの慣用句を覚えているだろうか?ジグは、透明であること、つまり魔法を使わないこと、あなたが読んだコードがそのまま反映されることを重要視している。

https://arne.me/blog/thoughts-on-zig

RustとZigの比較について

https://blog.logrocket.com/comparing-rust-vs-zig-performance-safety-more/

mena1mena1

なんでZigを作ったのか、Zigの概要について
https://www.youtube.com/watch?v=Gv2I7qTux7g

動画についての簡単なメモ

ソフトウェアが広く使われるには

  • ❌ ガベージコレクション: 予測不可能なストップ・ザ・ワールドの遅延不具合
  • ❌ 自動ヒープ割り当て:システムがメモリ不足になるとクラッシュまたはハングしてしまう
  • ❌ 書いたコードがCより遅ければ、誰かがCで書き直しちゃうよね
  • ❌ C ABIに対応していない場合、ほとんどの言語では利用できない
  • ❌ ソースからのビルドを複雑にする

ガベージコレクションのストップ・ザ・ワールドについて

ZigがGCを持たない設計により、この「ストップ・ザ・ワールド」の問題を根本的に回避している点です。
アプリケーションは完全に停止すること?
STW(Stop The World)
STWとは、GCによりアプリケーションが動作停止になる現象です。

アプリケーションのメモリー不足を防ぐため、Javaヒープ内の不要になったオブジェクトは、回収されなければならない。このプロセスはガベージ・コレクション(GC)として知られている。ガベージ・コレクションが行われるとき、ガベージ・コレクタはヒープへの排他的アクセスを取得しなければならない。この一時停止は、プロセスが完了するまでアプリケーションが停止しなければならないため、STW(Stop-the-World)一時停止と呼ばれることが多い。一般に、GCプロセスの最初のステップは、到達可能なオブジェクトにマークを付けることである。次のステップは、マークされていないオブジェクトを一掃してメモリーを取り戻すことである。最後のステップは、ヒープがひどく断片化されている場合にヒープをコンパクトにすることである。

https://haril.dev/jp/blog/2023/05/20/Garbage-Collection
https://kaworu.jpn.org/java/Stop_The_World
https://www.ibm.com/docs/en/semeru-runtime-ce-z/17?topic=management-garbage-collection-gc

自動ヒープ割り当てについて

C ABIについて

https://sosukesuzuki.dev/posts/abi-from-sksat-and-homekijitora/

高品質なソフトウェアを作るには

  1. 高品質なソフトウェア作りたい
  2. ほとんどのプログラミング言語は条件に満たしてない
  3. C++は複雑すぎる
  4. Cには大きな問題がある
  5. ZigはCの問題がある部分を修正している!!!!!!!

C vs Zig

mena1mena1

チュートリアルとして以下をやる

https://zig.guide/

Zigの.ignore

# This file is for zig-specific build artifacts.
# If you have OS-specific or editor-specific files to ignore,
# such as *.swp or .DS_Store, put those in your global
# ~/.gitignore and put this in your ~/.gitconfig:
#
# [core]
#     excludesfile = ~/.gitignore
#
# Cheers!
# -andrewrk

.zig-cache/
zig-out/
/release/
/debug/
/build/
/build-*/
/docgen_tmp/

# Although this was renamed to .zig-cache, let's leave it here for a few
# releases to make it less annoying to work with multiple branches.
zig-cache/

https://github.com/ziglang/zig/blob/master/.gitignore

mena1mena1

const -> immutable
var -> mutable

const constant = 5;
var variable: u32 = 5000;
std.debug.print("{d},{d}\n", .{ constant, variable });

//明示的な型強制
const inferred_constant = @as(i32, 20);
std.debug.print("{d}", .{inferred_constant});
mena1mena1

配列

const a = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
//_で配列のサイズを推測させる
const b = [_]u8{ 'h', 'w' };
std.debug.print("{s},{s}\n", .{ a, b }); //hello,hw
std.debug.print("{d}\n", .{a.len}); //5
mena1mena1

if

test "if statment" {
    const a = true;
    var x = 0;
    if (a) {
        x += 1;
    } else {
        x += 2;
    }
    try std.testing.expect(x == 1);
}

xの型を明示的に表さないとエラーが起きる

variable of type 'comptime_int' must be const or comptime

0がcomptime_int型として解釈される

comptime_int

コンパイル時に評価可能な整数値
特定の型(i32,u8..)に固定されておらず、汎用的にどの整数型にも適用できる
型は自由に推論されるが、ランタイム操作(プログラムの実行中に変数の値を変更したり、操作を加えたりすること)には使用されない

comptime_int型が誤用されることを防ぐために、明示的な型指定が必要
-> コンパイル時に決まるべき値と実行時に変化する値を明確に区別する必要がある

※ランタイム
プログラムを作成する場合、基本的な流れとしては以下となる

「開発」 > 「コンパイル(言語による)」 > 「実行」
https://zenn.dev/utah/articles/16df25f62536b0

ランタイム(実行時)とは、コンパイル済みのプログラムが実際に動作するタイミングを指す。CPUが命令を実行して、値を計算する。

  • 特徴:
    • 計算や処理がプログラムの実行中に行われる。
    • ランタイムで決定される値や操作を扱う(例えば、ユーザー入力やランダム値など)。
      -オーバーヘッド: 計算や処理がランタイムに行われるため、実行時のパフォーマンスに影響を与える可能性がある。

コンパイル時(Compile-time)
処理のタイミング: コンパイル時とは、ソースコードがバイナリに変換されるタイミングを指す。この時点で計算可能な値は、コンパイル時に計算される。

  • 特徴:
  • 値がコンパイル時に確定し、その結果がプログラムに直接埋め込まれる。
    • 実行時に計算が不要になるため、パフォーマンスが向上????????。
    • コンパイル時に計算可能なものは、comptimeキーワードを使うことで強制的にコンパイル時計算に移行可能。

comptimeという概念Zigにおいてかなり重要そう

https://zenn.dev/hastur/articles/0c00f676975e92
https://zenn.dev/smallkirby/articles/54882aee98e2c9

tryについて(エラーハンドリング)

https://zenn.dev/pgwalker/articles/16745c097e0140

mena1mena1

while

test "while" {
    var i: u8 = 2;
    while (i < 100) {
        i *= 2;
    }
    try std.testing.expect(i == 128);
}

continue

test "while with contiune" {
    var sum: u8 = 0;
    var i: u8 = 1;
    while (i <= 3) : (i += 1) {
        if (i == 2) continue;
        sum += i;
    }
    try std.testing.expect(sum == 4);
}

break

test "while with break" {
    var sum: u8 = 0;
    var i: u8 = 1;
    while (i <= 3) : (i += 1) {
        if (i == 2) break;
        sum += i;
    }
    try std.testing.expect(sum == 1);
}
mena1mena1

for


test "for" {
    const string = [_]u8{ 'a', 'b', 'c' };

    for (string, 0..) |character, index| {
        _ = character;
        _ = index;
    }

    for (string) |character| {
        _ = character;
    }

    for (string, 0..) |_, index| {
        _ = index;
    }

    for (string) |_| {}
}
mena1mena1

関数

全ての関数の引数は不変であり、コピーが必要な場合は明示的にする必要がある
プリミティブ型の引数は値渡し(コピー)

fn addFive(x: u32) u32 {
    return x + 5;
}

test "function" {
    const y = addFive(0);
    try std.testing.expect(@TypeOf(y) == u32);
    try std.testing.expect(y == 5);
}

参照渡しだと

fn addFiveRef(x: *u32) void {
    x.* += 5;
}

test "function" {
    var z: u32 = 5;
    try std.testing.expect(z == 5);
    addFiveRef(&z);
    try std.testing.expect(@TypeOf(z) == u32);
    try std.testing.expect(z == 10);
}

※値渡しと参照渡しのパフォーマンス
https://zenn.dev/btc/articles/240711_call_by_value_reference_memory

mena1mena1

Defer

現在のスコープに適用される
現在いるスコープから出るときに処理を実行する
複数のDeferがあるときは最後に定義したDeferから順に処理される

test "defer" {
    var x: i16 = 5;
    {
        defer x += 2;
        try std.testing.expect(x == 5);
    }
    try std.testing.expect(x == 7);
}

test "multi defer" {
    var x: f32 = 5;
    {
        defer x += 2;
        defer x /= 2;
    }
    try std.testing.expect(x == 4.5);
}

実用例

ポイントはdefer arena.deinit()でメモリの解放を行なっていること

const std = @import("std");

const Player = struct { x: i32, y: i32, health: i32 };

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const allocator = arena.allocator();
    defer arena.deinit();

    const ptr_player = try allocator.create(Player);
    ptr_player.x = 15;
    ptr_player.y = 25;
    ptr_player.health = 100;

    std.debug.print("Player Health: {d}\n", .{ptr_player.health});
}

deferで解放を行わないとどうなるのか?

  1. 解放漏れ
    正常に終了するんだったら問題ないんだけど、途中でエラーが発生してしまった場合arena.deinit()を呼ばないまま関数を抜けてしまう可能性がある
  2. コードの複雑化
    解放を忘れないように各分岐や早期リターン箇所に arena.deinit() を挿入する必要がある
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const allocator = arena.allocator();

    const ptr_player = try allocator.create(Player);
    ptr_player.x = 15;
    ptr_player.y = 25;
    ptr_player.health = 100;

    std.debug.print("Player Health: {d}\n", .{ptr_player.health});

    // 解放を忘れたり、エラー時にスキップするリスクがある
    arena.deinit();
}

deferを使うことで解放が1箇所に集中する・エラーが起きても自動で解放されるため、安全性が向上
https://medium.com/@eddo2626/lets-learn-zig-3-how-to-use-defer-a3d2555c47ae

mena1mena1

エラー

const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};
const AllocationError = error{OutOfMemory};

test "coerce error from a subset to a superset" {
    const err: FileOpenError = AllocationError.OutOfMemory;
    try std.testing.expect(err == FileOpenError.OutOfMemory);
}

test "error union" {
    //Allocation型のエラーとエラーが発生しなかった場合の値としてu16
    const maybe_error: AllocationError!u16 = 10;
    //エラーが発生した場合にcatchの後に指定した値を返す
    const no_error = maybe_error catch 0;

    try std.testing.expect(@TypeOf(no_error) == u16);
    try std.testing.expect(no_error == 10);
}

const expect = @import("std").testing.expect;

fn failingFunction() error{Oops}!void {
    return error.Oops;
}

fn failFn() error{Oops}!i32 {
    try failingFunction();
    return 12;
}

tryは以下のような意味を持つ糖衣構文

const result = x catch |err| return err;
fn failingFunction() error{Oops}!void {
    return error.Oops;
}

fn failFn() error{Oops}!i32 {
    try failingFunction();
    return 12;
}

test "try" {
    const v = failFn() catch |err| {
        try std.testing.expect(err == error.Oops);
        return;
    };
    try std.testing.expect(v == 12); // is never reached
}
mena1mena1

switch

test "switch statement" {
    var x: i8 = 10;
    switch (x) {
        -1...1 => {
            x = -x;
        },
        10, 100 => {
            //整数の除算を行う際に、割り切れることを保証
            //割り切れない場合ランタイムエラー
            x = @divExact(x, 10);
        },
        else => {},
    }
    try std.testing.expect(x == 1);
}
test "switch expression" {
    var x: i8 = 10;
    x = switch (x) {
        -1...1 => -x,
        10, 100 => @divExact(x, 10),
        else => x,
    };
    try std.testing.expect(x == 1);
}
mena1mena1

ランタイムの安全性


test "out of bounds" {
    const a = [3]u8{ 1, 2, 3 };
    var index: u8 = 5;
    const b = a[index];
    _ = b;
    _ = &index;
}

//unsafe状態
test "out of bounds, no safety" {
    @setRuntimeSafety(false);
    const a = [3]u8{ 1, 2, 3 };
    var index: u8 = 5;
    const b = a[index];
    _ = b;
    _ = &index;
}

unreachable : 到達不可能であることを保証・このコードに到達した場合はエラーを発生

test "unreachable" {
    const x: i32 = 1;
    const y: u32 = if (x == 2) 5 else unreachable;
    _ = y;
}
mena1mena1

ポインタ

Zigのノーマルポインタは0またはnullを持つことができない


fn increment(num: *u8) void {
    num.* += 1;
}

test "pointers" {
    var x: u8 = 1;
    increment(&x);
    try std.testing.expect(x == 2);
}
test "naughty pointer" {
    const x: u16 = 0;
    const y: *u8 = @ptrFromInt(x);
    _ = y;
}
❯ zig test src/main.zig
src/main.zig:251:32: error: pointer type '*u8' does not allow address zero
    const y: *u8 = @ptrFromInt(x);
 

const 変数を参照すると、constポインタが生成され、ポインタ経由で指す値は変更できない

test "const pointers" {
    const x: u8 = 1;
    const y = &x;
    y.* += 1;
}
mena1mena1

slices

x[n..m] は配列からスライスを作成する
x[n]から始まり、x[m-1]で終わる要素のスライスを作成する

const array = [_]u8{1,2,3};
//slice = [1,2]が作られる
const slice = array[0..2]
//slice = [1,2,3] (配列の中を全て取ってスライスが作られる)
const slice2 = array[0..];
//固定長配列 *const [N]Tをスライス []const T に暗黙的に変換
// ただし[]T(可変スライス)だと変換できないためエラーが発生する(values: []u8)
fn total(values: []const u8) usize {
    var sum: usize = 0;
    for (values) |v| sum += v;
    return sum;
}

test "slices" {
    const array = [_]u8{ 1, 2, 3, 4, 5 };
    const slice = array[0..3];//*const [3]u8
    try std.testing.expect(total(slice) == 6);
}
test "slices2" {
    const array = [_]u8{ 1, 2, 3, 4, 5 };
    const slice = array[0..3];
    try std.testing.expect(@TypeOf(slice) == *const [3]u8);
}
mena1mena1

Enum

ある値マッピングされる名前付きシンボル
enumにおいて、列挙子には連続した整数値が自動的に割り当てられている

const Value = enum(u2) {
    zero,
    one,
    two,
    three,
};

test "enum oridinal value" {
    try std.testing.expect(@intFromEnum(Value.zero) == 0);
    try std.testing.expect(@intFromEnum(Value.one) == 1);
    try std.testing.expect(@intFromEnum(Value.two) == 2);
    try std.testing.expect(@intFromEnum(Value.three) == 3);
}
const Value2 = enum(u32) {
    hundred = 100,
    thousand = 1000,
    millison = 1000000,
    next,
};

test "set enum oridinal value" {
    try std.testing.expect(@intFromEnum(Value2.hundred) == 100);
    try std.testing.expect(@intFromEnum(Value2.thousand) == 1000);
    try std.testing.expect(@intFromEnum(Value2.millison) == 1000000);
    //列挙子自動割り当てにより、前の列挙子の値に+1した値が
    //自動的に次の列挙子に割り当てられる
    try std.testing.expect(@intFromEnum(Value2.next) == 1000001);
}

enumにメソッドを追加できる

const Suit = enum {
    clubs,
    spades,
    diamonds,
    hearts,
    pub fn isClubs(self: Suit) bool {
        return self == Suit.clubs;
    }
};

test "enum method" {
    try std.testing.expect(Suit.spades.isClubs() == Suit.isClubs(.spades));
}

enum内にvar,constを宣言することができる

const Mode = enum {
    //Modeのどのインスタンス(Mode.on,Mode.off)とも関係なく
    //Mode自体が持つ「グローバルな状態」として振る舞う
    var count: u32 = 0;
    on,
    off,
};

test "hmm" {
    Mode.count += 1;
    try std.testing.expect(Mode.count == 1);
}
mena1mena1

struct

const Vec3 = struct { x: f32, y: f32, z: f32 };

test "struct usage" {
    const my_vector = Vec3{
        .x = 0,
        .y = 100,
        .z = 50,
    };
    _ = my_vector;
}
test "missing struct field" {
    const my_vector = Vec3{
        .x = 0,
        .z = 50,
    };
    _ = my_vector;
}
src/main.zig:347:27: error: missing struct field: y
    const my_vector = Vec3{
                      ~~~~^
src/main.zig:335:14: note: struct 'main.Vec3' declared here
const Vec3 = struct { x: f32, y: f32, z: f32 };

フィールドにはデフォルト指定ができる

const Vec4 = struct { x: f32 = 0, y: f32 = 0, z: f32 = 0, w: f32 = 0 };

test "struct defaults" {
    const my_vector = Vec4{
        .x = 25,
        .y = -50,
    };
    _ = my_vector;
}
const Stuff = struct {
    x: i32,
    y: i32,
    //*Stuff self.が指す構造体のフィールドを変更可能に(参照渡し:元のデータに対する操作)
    fn swap(self: *Stuff) void {
        const tmp = self.x;
        self.x = self.y;
        self.y = tmp;
    }
};

test "automatic dereference" {
    var thing = Stuff{ .x = 10, .y = 20 };
    thing.swap();
    try std.testing.expect(thing.x == 20);
    try std.testing.expect(thing.y == 10);
}
mena1mena1

union

1つのメモリ領域を複数の異なる型で共有する
1つの型しか有効な状態として保持されない


const Result = union {
    int: i64,
    float: f64,
    bool: bool,
};

test "simple union" {
    var result = Result{ .int = 1234 };
    result.float = 12.34;
    try std.testing.expect(result.int == 1234);
}
thread 5666156 panic: access of union field 'float' while field 'int' is active

タグ付きunion

どのフィールドが有効であるかを型システムが追跡できるため、安全性が上がる

const Tag = enum { a, b, c };
const Tagged = union(Tag) { a: u8, b: f32, c: bool };

test "switch on tagged union" {
    var value = Tagged{ .b = 1.5 };
    switch (value) {
        .a => |*byte| byte.* += 1,
        .b => |*float| float.* *= 2,
        .c => |*b| b.* = !b.*,
    }
    try std.testing.expect(value.b == 3);
}

以下のようにコンパクトにかける

const Tagged = union(enum) { a: u8, b: f32, c: bool };
mena1mena1

integer rule

目的の型に強制できない整数の値が格納されている場合は、@intCast を使用して、ある型から他の方へ明示的に変換できる

test "@intCast" {
    const x: u64 = 200;
    const y = @as(u8, @intCast(x));
    try std.testing.expect(@TypeOf(y) == u8);
}

ノーマル演算子とラッピング演算子

  • ノーマル演算子
    • オーバーフローが発生した場合にランタイムエラーを発生させる
    test "int overflow" {
        var a: u8 = 255;
        a += 1;
        try std.testing.expect(a == 256);
    }
    
    src/main.zig:414:7: 0x100db4553 in test.int overflow (test)
    a += 1;
    
  • ラッピング演算子
    • オーバーフローが発生してもエラーを発生させず、値を循環させる
    • オーバーフローが発生すると数値が0に戻る
      test "well defined overflow" {
          var a: u8 = 255;
          a +%= 1;
          try std.testing.expect(a == 0);
      }
    
mena1mena1

Labelled

Labelled Block

ブロックにラベルをつけて、特定のブロックから値を返す

test "labelled blocks" {
    const count = blk: {
        var sum: u32 = 0;
        var i: u32 = 0;
        while (i < 10) : (i += 1) sum += i;
        break :blk sum;
    };
    try std.testing.expect(count == 45);
    try std.testing.expect(@TypeOf(count) == u32);
}

Labelled Loop

test "nested continue" {
    var count: usize = 0;
    outer: for ([_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }) |_| {
        // 内側のループは毎回1回目のみ実行される
        for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
            count += 1;
            continue :outer;
        }
    }
    //外側の反復回数は8、内側は1回目のみ実行されるので8となる
    try std.testing.expect(count == 8);
}
mena1mena1

Loops as Expressions

ループの結果を直接戻り値として利用することができる

fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
    var i = begin;
    return while (i < end) : (i += 1) {
        if (i == number) {
            break true;
        }
    } else false;
}

test "while loop expression" {
    try std.testing.expect(rangeHasNumber(0, 10, 3));
}
mena1mena1

optional

?Tで表し、nullを許容する

test "optional" {
    var found_index: ?usize = null;
    const data = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 12 };
    for (data, 0..) |v, i| {
        if (v == 8) found_index = i;
    }

    try std.testing.expect(found_index == 7);
}

optionalはorelse式を使える
optionalの値がnullの場合に代替の値を返すために使う

test "orelse" {
    const a: ?f32 = null;
    const fallback_value: f32 = 0;
    const b = a orelse fallback_value;
    try std.testing.expect(b == 0);
    try std.testing.expect(@TypeOf(b) == f32);
}

.?orelse unreachableの省略形
unreachableは絶対に実行されないコードであることを明示するキーワードで、.?がnullだったらランタイムエラー

test "orelse unreachable" {
    const a: ?f32 = 5;
    const b = a orelse unreachable;
    const c = a.?;
    try std.testing.expect(b == c);
    try std.testing.expect(@TypeOf(c) == f32);
}

optinalのポインタやスライスが余分なメモリを消費しないのは、nullを内部的にゼロ値(0)として扱うから
-> zigのnull pointerの仕組み
他の言語だと、どのように扱うんだろう?
生成AIによると、以下のようになるらしい

例: RustのOption や C++のstd::optional
通常は「値」と「存在フラグ」(bool型)を持つため、余分なメモリ(フラグ分)が必要になります。
mena1mena1

Comptime

fn fibonaccci(n: u16) u16 {
    if (n == 0 or n == 1) return n;
    return fibonaccci(n - 1) + fibonaccci(n - 2);
}

test "comptime blocks" {
    const x = comptime fibonaccci(10);
    const y = comptime blk: {
        break :blk fibonaccci(10);
    };
    try std.testing.expect(y == 55);
    try std.testing.expect(x == 55);
}
test "comptime_int" {
    //comptime_int
    const a = 12;
    //comptime_int
    const b = a + 10;

    const c: u4 = a;
    const d: f32 = b;

    try std.testing.expect(c == 12);
    try std.testing.expect(d == 22);
}

comptime_intについて
https://zenn.dev/link/comments/78746aec5ddbb5

fn Matrix(
    comptime T: type,
    comptime width: comptime_int,
    comptime height: comptime_int,
) type {
    return [height][width]T;
}

test "returning a type" {
    try std.testing.expect(Matrix(f32, 4, 4) == [4][4]f32);
}
fn addSmallInts(T: type, a: T, b: T) T {
    return switch (@typeInfo(T)) {
        .ComptimeInt => a + b,
        .Int => |info| if (info.bits <= 16)
            a + b
        else
            @compileError("ints too large"),
        else => @compileError("only ints accepted"),
    };
}

test "typeinfo switch" {
    const x = addSmallInts(comptime_int, 20, 30);
    try std.testing.expect(@TypeOf(x) == comptime_int);
    try std.testing.expect(x == 50);
}

Comptimeでできること

https://www.revelo.com/blog/metaprogramming

https://www.openmymind.net/Basic-MetaProgramming-in-Zig/

https://zenn.dev/hastur/articles/0c00f676975e92

mena1mena1

Bultin Functions

@ で始まるのは Builtin Functions ですね。 @as は広い型にキャストする用で、狭い型へのキャストで情報落ちの際はパニックさせるなら @intCast、狭い型へのキャストではみ出たビットは破棄するなら @truncate と使い分けが必要です。
https://zenn.dev/link/comments/c829c2771d0729

mena1mena1

Payload Captures

Payload Capturesを使う場合

const std = @import("std");

pub fn main() void {
    const maybe_num: ?usize = 10;

    if (maybe_num) |n| {
        std.debug.print("Value: {}\n", .{n});
    } else {
        std.debug.print("No value\n", .{});
    }
}

Payload Capturesを使わない場合

const std = @import("std");

pub fn main() void {
    const maybe_num: ?usize = 10;

    if (maybe_num != null) {
        const n = maybe_num.?; // 強制アンラップ
        std.debug.print("Value: {}\n", .{n});
    } else {
        std.debug.print("No value\n", .{});
    }
}

Payload Captureの良い点

  • 安全性:
    if (maybe_num) |n|を使えば、値が存在する場合にのみ中身が利用可能なため、nullの扱いを間違えるリスクがない
  • コードの簡潔さ:
    条件分岐とペイロードのアンラップを一体化して記述できるため、コードが短くなる

使わない方の利点

  • 強制アンラップ(.?)
    条件分岐なしでも使用可能: ペイロードキャプチャを使わない場合は、if文を必要としない箇所でもmaybe_num.?を使ってアンラップできる

  • より明示的:
    オプショナル型を操作していることが明確であり、直感的に理解しやすい場合がある

test "for capture" {
const x = [_]i8{ 1, 5, 120, -5 };
for (x) |v| try std.testing.expect(@TypeOf(v) == i8);
}

mena1mena1

Inline Loops

test "inline for" {
    const types = [_]type{ i32, f32, u8, bool };
    var sum: usize = 0;
    inline for (types) |T| sum += @sizeOf(T);
    try std.testing.expect(sum == 10);
}

コンパイル時に展開されるループ

Using these for performance reasons is inadvisable unless you've tested that explicitly unrolling is faster; the compiler tends to make better decisions here than you.

使うことはそんなになさそう

mena1mena1

Opaque


const Window = opaque {
    fn show(self: *Window) void {
        show_window(self);
    }
};
//externは、外部(C言語など)の関数を宣言する
//callconv(.C)は、関数の呼び出し規約がC言語であることを示す
extern fn show_window(*Window) callconv(.C) void;

test "opaque with declarations" {
    var main_window: *Window = undefined;
    main_window.show();
}

Cで定義されたshow_window関数の実装がないため、testはエラー

不透明型(ゼロではないが)サイズと配置が不明。このため、これらのデータ型を直接保存することはできない。これらは、情報がない型へのポインタを使用してタイプ セーフを維持するために使用される。(外部ライブラリやAPIと連携する際に便利)

不透明型の一般的な使用例は、完全な型情報を公開しない C コードと相互運用するときに型安全性を維持する

mena1mena1

Anonymous Structs


test "anonymous struct literal" {
    const Point = struct { x: i32, y: i32 };

    const pt: Point = .{
        .x = 13,
        .y = 67,
    };
    try expect(pt.x == 13);
    try expect(pt.y == 67);
}
mena1mena1

Sentinel Termination

test "sentinel termination" {
    //[N:t]T センチネルの構文
    const terminated = [3:0]u8{ 3, 2, 1 };
    try std.testing.expect(terminated.len == 3);
    //メモリ上の4番目の要素(インデックス3)が0
    try std.testing.expect(@as(*const [4]u8, @ptrCast(&terminated))[3] == 0);
}
  • センチネル値(Sentinel Value)
    配列やリストの終端を示すために予約された特定の値。この値は通常、配列内の通常のデータとは異なる値に設定される。
mena1mena1

Vectors

const meta = @import("std").meta;

test "vector add" {
    const x: @Vector(4, f32) = .{ 1, -10, 20, -1 };
    const y: @Vector(4, f32) = .{ 2, 10, 0, 1 };
    const z = x + y;
    try expect(meta.eql(z, @Vector(4, f32){ 3, 0, 20, 0 }));
}

It is worth noting that using explicit vectors may result in slower software if you do not make the right decisions - the compiler's auto-vectorisation is fairly smart as-is.