Zig
melangの各noteで他言語と比較した、Zigの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
型推論があり、型そのものをcomptime値として扱える。
const age: i32 = 20;const age: i32 = 20;
const name = "Alice"; // 型推論で *const [5:0]u8 と判定される
fn Box(comptime T: type) type {
return struct { value: T };
}`const`/`var`宣言では型注釈を省略でき、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が初期化式から型を推論する。他言語と異なるのは型(`type`)自体が値として扱われ、`comptime T: type`のように関数引数として渡せる点で、GenericsはRustのような専用構文ではなく、この`comptime`パラメータを使ったコンパイル時のダックタイピングオブジェクトの型そのものではなく、そのオブジェクトが実際に持つメソッドやプロパティ(振る舞い)で扱いを決める考え方。「アヒルのように鳴き、アヒルのように歩くならアヒルとみなす」という比喩に由来し、動的型付け言語でよく採用される。として実現される。
型注釈の位置
Rust/Go系と同じpostfix構文。戻り値には矢印を使わず、関数シグネチャは常に明示。
const age: i32 = 20;const age: i32 = 20; // 変数宣言は省略可能
const name = "Alice"; // 型推論で *const [5:0]u8
fn greet(name: []const u8) []const u8 { // 引数・戻り値は常に明示(矢印は使わない)
return name;
}`name: Type`というpostfix構文で、`const`/`var`宣言は初期化式から型推論できる。関数は`fn f(args) ReturnType`という形で、Goのように戻り値の型に矢印を使わず引数リストの直後に置くが、Rust/Goと同様に引数・戻り値は推論できず常に明示が必要。
型変換
builtinの変換関数で明示し、失敗し得るパースはエラーユニオンで表す。
const y: i64 = @intCast(x);const x: i32 = 10;
const y: i64 = @intCast(x); // 拡大方向の変換
const big: i64 = 3_000_000_000;
const z: i32 = @intCast(big); // 範囲外だとDebug/ReleaseSafeで実行時panic
const w: i32 = @truncate(big); // 切り捨てを明示するbuiltin
const n: i32 = try std.fmt.parseInt(i32, "42", 10); // 失敗は!i32のエラーユニオン`@intCast`, `@floatCast`, `@truncate`, `@ptrCast`のようなbuiltin関数で変換を明示する。数値の縮小変換は安全性チェック付きビルド(Debug/ReleaseSafe)では範囲外の値を実行時panicとして検出し(ReleaseFast/ReleaseSmallでは未定義動作)、切り捨てを許容したい場合は`@truncate`を使い分ける。文字列のパースなど失敗し得る変換は`std.fmt.parseInt`のようにerror union(`!T`)を返す関数で表現し、`try`で伝播するか`catch`で処理する。C由来の暗黙の数値変換はほとんどなく、comptime-known値やpeer type resolutionによる限定的なcoercionを除けば変換は明示のbuiltinを介する。
暗黙の型変換
実行時値の数値変換は暗黙にはできないが、comptime値は自動でcoerceされる。
const x: i32 = 10;const x: i32 = 10;
const y: i64 = x; // コンパイルエラー:runtime値は暗黙変換されない
const y: i64 = @intCast(x); // OK
const z: i64 = 10; // OK:10はcomptime_intなので変換先の型に自動でcoerceされる実行時に決まる値同士の数値型間ではRust/Goと同様に暗黙変換を持たず、i32からi64のような拡大変換であっても`@intCast`などのbuiltinで明示する必要がある。一方、リテラルのようなcomptime_int/comptime_float型のcomptime-known値は、収まる範囲であれば変換先の型へ自動的にcoerceされる。さらにif/elseの各分岐や配列・スライスの型を揃えるpeer type resolutionという別の仕組みもあり、「暗黙変換が一切ない」と単純化はできない。
Enum / Sum Type
enumとunionを組み合わせたunion(enum)でtagged unionを表現する。
const Result = union(enum) { success: User, failure: []const u8 };const Result = union(enum) {
success: User,
failure: []const u8,
};
fn describe(r: Result) []const u8 {
return switch (r) {
.success => |user| user.name,
.failure => |message| message,
}; // 全variantを網羅しないとコンパイルエラー
}`enum`は整数値ベースの単純な列挙、`union`はvariantごとに異なる型を持てるが現在どのフィールドが有効かを実行時に保持しない生のunion。両者を組み合わせた`union(enum)`が、有効なvariantをタグとして安全に追跡できるtagged unionで、Rustの`enum`と完全に同じ構文ではないが同等のADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。/Sum Typeを表現できる。`switch`は`union(enum)`の全variantを網羅しないとコンパイルエラーになる(`else`を書けば任意の網羅で許容される)。
値の不変性
`const`/`var`を明確に区別し、ポインタのconstnessも型に現れる。
const x = 1; / var x = 1;const v = [_]i32{ 1, 2 }; // const: 要素も変更できない
// v[0] = 9; // コンパイルエラー
var w = [_]i32{ 1, 2 };
w[0] = 9; // OK: varな配列
fn increment(p: *i32) void {
p.* += 1; // 非constポインタ経由でのみ書き込める
}`const`と`var`が束縛の可変性を明確に分け、`const`は再代入も中身の変更もできない。ポインタも`*T`(書き込み可)と`*const T`(読み取り専用)で型として区別され、`*const T`経由では書き込めない。Rustの借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。チェッカのような所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。規則とは別物で、複数の可変ポインタが同時に存在することを言語が防ぐ仕組みではない点に注意。
名前付き引数
名前付き引数はなく、デフォルト値を持つstructをanonymous struct literalで渡す慣習で代替する。
fn foo(opts: struct { width: i32, height: i32 = 0 }) i32const Options = struct {
width: i32,
height: i32 = 0, // デフォルト値を持てる
};
fn foo(opts: Options) i32 {
return opts.width + opts.height;
}
foo(.{ .width = 10, .height = 20 }); // anonymous struct literalで意味を明示
foo(.{ .width = 10 }); // heightはデフォルト値を使う関数呼び出しに名前付き引数の構文はなく、位置引数のみ。標準ライブラリではフィールドにデフォルト値を持つ`Options`のようなstructを1つの引数として受け取り、呼び出し側は型名を省略できる`.{ .field = value }`というanonymous struct literalでフィールド名を明示しつつ、省略したフィールドはデフォルト値に委ねる慣習が広く使われる。C/Goの「構造体で代替」と同じ発想だが、フィールドのデフォルト値と組み合わせられる点が異なる。
関数の引数
引数は値渡しで関数内では不変。大きな構造体の受け渡しはABI最適化の対象。
fn f(x: i32) void / fn f(p: *i32) void / fn f(items: []i32) voidfn a(n: i32) void {
// n = 1; // コンパイルエラー:引数は既定でconst
var m = n;
m += 1; // 変更したい場合はローカルのvarへ明示的にコピーする
}
fn b(p: *i32) void {
p.* += 1; // ポインタ経由で書き換える
}
var x: i32 = 0;
a(x);
b(&x); // 呼び出し側に & が付く
fn c(items: []i32) void {
items[0] = 9; // スライスは(ポインタ, 長さ)の組で、中身は共有される
}引数は既定で値渡しかつ関数内では不変(const相当)で、書き換えたい場合はローカルの`var`へ明示的にコピーする必要がある。ポインタ・スライスで受け取れば呼び出し側の値を書き換えられ、呼び出し側にも`&`が付く点はGo/Cと同じ。struct/arrayのような大きな集約型は、意味論上は値渡し(コピー)として扱われるが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。がABIレベルの最適化として実体を参照渡しに置き換えることがある——これは言語仕様ではなく実装上の最適化であり、呼び出し側から見える意味論(値が共有されないこと)には影響しない。allocationが必要な標準ライブラリ関数はallocatorを明示的な引数として受け取る慣習があり、隠れたメモリ確保を避ける設計思想の一部になっている。
if 文/式
`if`は式。値として使うときは`else`が必須で、statementとしては`else`なしで書ける。
const x = if (cond) a else b;const x = if (cond) 1 else 2;
// 値として使うならelseが必要
// const y = if (cond) 1; // コンパイルエラー
if (cond) {
process();
}`if`は式で、値として使う場合は`else`が必須になる。この点はKotlinと同じである。手続きの`else`は禁止されていない。
I/O の明示
型でI/Oを区別する仕組みはないが、allocator・writerを引数で明示する規約がある。
fn findUser(allocator: std.mem.Allocator, id: UserId) !Userfn findUser(allocator: std.mem.Allocator, id: UserId) !User {
return db.get(allocator, id);
}
fn square(allocator: std.mem.Allocator, x: i32) i32 {
_ = findUser(allocator, .{}) catch {}; // 呼べてしまう。コンパイラは止めない
return x * x;
}ある関数がI/Oを行うかどうかを型シグネチャで区別する仕組みはなく、純粋な処理からI/Oを呼び出すことも制限されない。一方でZigは「no hidden control flow」「no hidden memory allocation」を設計思想として掲げており、allocationが必要な標準ライブラリ関数はallocatorを、出力を行う関数はwriterを、それぞれ明示的な引数として受け取るのが標準的な作法になっている。これはHaskellの`IO`モナド値を文脈(コンテナ)に包んだまま、その文脈を保って値を合成するための抽象化。Haskellでは`IO`型がI/Oという文脈を表すモナドで、`do`記法によってIOアクションを順に合成する。のような型レベルのeffect追跡ではなく「何に依存して動くか」を関数シグネチャに明示する規約であり、I/Oかどうかそのものを区別するわけではない。失敗し得る処理はerror union(`!T`)で表現されるが、それがI/Oによるものかは区別されない。
言語名
一般名詞に由来しない短い固有の名前で、拡張子・コマンド名・言語名が一致する。
`Rust`や`Kotlin`と同様に一般名詞ではない短い名前で、`.zig`という拡張子・`zig`というCLIコマンド名・言語名がすべて一致しており、Go(`Golang`という別名が必要)やSwift(既存製品名との衝突)のような問題を持たない。命名の由来について、詳細を裏付ける公式一次情報は確認できていないため、具体的な語源はここでは断定しない。MoonBit同様、新興言語としてドメイン(ziglang.org)や名前空間を早期から一意に確保できた例。