7 ポイント 投稿者 GN⁺ 2025-08-22 | 1件のコメント | WhatsAppで共有
  • ZigはRustに似た波かっこベースの構文を土台にしつつ、より単純な言語意味論と洗練された構文上の選択によって改善されている
  • 整数リテラルはすべての型がcomptime_intとして始まり、代入時に明示的に変換され、文字列リテラル\\ベースの簡潔な生文字列記法を使う
  • .x = 1形式のレコードリテラルはフィールド書き込みを検索しやすくし、すべての型は前置記法で一貫して表現される
  • andorを制御フローのキーワードとして使い、if・loop構文では必要に応じて波かっこを省略でき、フォーマッタが安全性を保証する
  • 名前空間なしですべてを式として扱うことで型・値・パターン構文を統合し、ジェネリクス・レコードリテラル・組み込み関数(@import@asなど)を簡潔に活用する

概要

  • ZigはRustに似た見た目を持ちながら、より単純な言語構造を採用している
  • 構文設計ではgrepしやすさ構文の一貫性不要な視覚ノイズの削減に注力している

整数リテラル

const an_integer = 92;  
assert(@TypeOf(an_integer) == comptime_int);  
  
const x: i32 = 92;  
const y = @as(i32, 92);  
  • すべての整数リテラルはcomptime_int
  • 変数に代入する際は明示的に型を指定するか、@asを使って変換する
  • var x = 92;の形は動作せず、明示的な型が必要

文字列リテラル

const raw =  
    \\Roses are red  
    \\  Violets are blue,  
    \\Sugar is sweet  
    \\  And so are you.  
    \\  
;  
  • 各行が個別のトークンなのでインデントの問題がない
  • \\自体をエスケープする必要がない

レコードリテラル

const p: Point = .{  
    .x = 1,  
    .y = 2,  
};  
  • .x = 1形式は読み取りと書き込みの区別に有利
  • .{}記法はブロックと区別しつつ、結果の型へ自動変換される

型記法

u32        // 整数  
[3]u32     // 長さ3の配列  
?[3]u32    // null可能な配列  
*const ?[3]u32 // 定数ポインタ  
  • すべての型は前置(prefix)記法
  • 逆参照は後置記法(ptr.*

識別子

const @"a name with space" = 42;  
  • キーワード衝突を避けたり、特殊な名前を指定したりできる

関数宣言

pub fn main() void {}  
fn add(x: i32, y: i32) i32 {  
    return x + y;  
}  
  • fnキーワードと関数名が隣接しており、検索しやすい
  • 戻り値型の表記に->を使わない

変数宣言

const mid = lo + @divFloor(hi - lo, 2);  
var count: u32 = 0;  
  • constvarを使用
  • 型表記は名前: 型の順

制御フロー: and/or

while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {  
    count -= 1;  
}  
  • andorは制御フローのキーワード
  • ビット演算には&|を使う

if文

.direction = if (prng.boolean()) .ascending else .descending;  
  • 丸かっこは必須、波かっこは任意
  • zig fmtが安全なフォーマットを保証する

ループ

for (0..10) |i| {  
    print("{d}\n", .{i});  
} else @panic("loop safety counter exceeded");  
  • forwhileはいずれもelse節をサポート
  • イテレータと要素名が直感的に配置されている

名前空間と名前解決

const std = @import("std");  
const ArrayList = std.ArrayList;  
  • 変数のシャドーイングは禁止
  • 名前空間やグロブインポートはない

すべては式

const E = enum { a, b };  
const e: if (true) E else void = .a;  
  • 型・値・パターン構文を統合している
  • 型の位置に条件式を置ける

ジェネリクス

fn ArrayListType(comptime T: type) type {  
    return struct {  
        fn init() void {}  
    };  
}  
  
var xs: ArrayListType(u32) = .init();  
  • ジェネリクスは関数呼び出し構文(Type(T))で表現する
  • 型引数は常に明示する

組み込み関数

const foo = @import("./foo.zig");  
const num = @as(i32, 92);  
  • @接頭辞でコンパイラ提供機能を呼び出す
  • @importはファイルパスを明確に示す
  • 引数は必ず文字列リテラルでなければならない

結論

  • Zigの構文は小さな選択の積み重ねによって読みやすい言語を作り上げた事例
  • 機能数を減らせば必要な構文も減り、構文同士の衝突可能性も低下する
  • 既存言語の優れたアイデアを取り入れつつ、必要なときには大胆に新しい構文を導入している

1件のコメント

 
GN⁺ 2025-08-22
Hacker Newsのコメント
  • この記事は文法設計で生じるさまざまなトレードオフを深く扱っていて、Zigの文法が持つミニマリズムと一貫性、そして無慈悲なほど可読性に集中している点が本当に印象的だった。これは抽象的な美しさではなく、産業用途において驚く要素のない「ブルータリズム」だという点が気に入っている。こういうバランスの取れた文法設計は本当に珍しく、Zigはそれをうまくやったと思う

    • 記事でエラーハンドリングへの言及がなかったのは残念。Zigのtry/catch方式は非常に優れていて、多くの言語の中でも一番好きなエラーハンドリング方法だ。この点も紹介されていればさらに良かったと思う

    • 「表面的に美しい可読性」ではなく、抽象化によって得られる一貫した美しさこそがZigの本当の魅力だ。S式とM式の比喩のように、一般的なケースでの良いアプローチは、多数の例外的状況のための特殊な設計よりも長期的には良いことが多い。C++のように各種の例外ケースを追加していくと、結局はすべての規則を覚えなければならない負担だけが増える。言語設計では単純さと一貫性を追求すると、最終的に複雑さをユーザーに負わせる「Turing tarpit」に陥ることがあるので、一般的な規則から特殊ケースが自然に解決されるアプローチが重要だ。XKCDの New Pet コミックにもこうした例が見られる

    • 印象に残った例があれば共有してもらえるとうれしい

  • ZigがRustのように「名前:型」形式で型を明示する点については、むしろ型が先に来る伝統的な方式のほうが好きだ。変数宣言を見直すときに一番知りたいのはその変数の型で、これを素早く見つけられないと不便だ。特にRustでは let mut のように不要に繰り返される要素が多く、かえって煩わしいし、CやC++のように型が先に来るのも良い。実際には型推論が必要なところにだけ最小限で使うのが理想だと思う

    • let キーワードは、それが宣言文であることを明確にしてくれるので必要な面もある。そうでないとC++の曖昧な構文解析問題に悩まされることになる

    • 私もいつも変数の型を先に確認しようとするので、型が前に来る方式を好む。構文解析器にとっては名前を先に処理するほうが都合がよく、TypeScriptがJavaScriptとの互換性のためにこの構造を採用した点も理解できる。結局重要なのは使いやすい標準ライブラリだと思う。型システムを過剰に悪用する例のように、わざわざすべての状態を型で表現するより、意図を明確に伝えることのほうが大事だ

    • コードで変数型を確認しようとして上に戻ることはあるが、逆に型が先にあると探している変数宣言を見つけにくくなる。型名がいちばん前にあり、その長さも可変なので、視線を左右に何度も動かさなければならず、非効率に感じる

    • たいていの場合、エディタでマウスオーバーすれば型情報をすぐ表示できるので、コード内で型の位置はそれほど重要ではないかもしれない。Rustが冗長なのは、構文解析の曖昧さを避けるという実装上の側面が大きい。CやC++のように型が先に来ると、特定の名前で宣言された変数をgrepで簡単に探しにくくなり、return typeを先頭に置くスタイルはテンプレートのために導入されたものだが、場合によってはコードをより読みやすく、探しやすくしてくれる

    • 個人的にはパスカル風の型注釈方式のほうが好きだ。型推論を使うときにも別の auto のような迂回的機能が不要で、構文解析の観点でも曖昧さが少ない。MyClass x ではMyClassが型なのか変数名なのかすぐには分からないので、そうした曖昧さを減らしてくれる

  • Zigのraw/multiline string(複数行文字列)文法で、\を何度も書かなければならない方式はあまりに混乱しやすく極端に感じる

    • Python、C++、Rustなどで複数行文字列を整形したことがあるなら、その不便さは分かるはずだ。インデントが文字列内容に含まれてしまう問題があるのでいつも悩まされるし、YAMLのようにインデント除去モードがある場合はかえって混乱を増す。Zigの方式はインデントに関して非常に明快だ

    • 最初はこの文法がとても不便に感じたが、Zigを使っていくうちにだんだん慣れて、むしろ利点が見えてきた。Zigは新鮮な分、最初は好みに合わないことがあっても、実際に使うと長所が分かってくる

    • 実際には狂った文法なのではなく、この複雑な問題(複数行文字列の中にさらに複数行文字列を安全に入れる問題)が狂っているのだ。Zigでは別途エスケープも不要で、インデントも気にしなくてよいのが良い

    • KotlinのtrimIndent、GoやJavaのテキストブロック、そして特にGoのbacktick raw stringのほうが自分にはずっと自然に感じる。Zigでは \ のせいで、むしろ @embedFile 方式で回避して使っている

    • 見た目として \ は好きではないが、複数行リテラルとインデント問題をきれいに解決する方法だと思う。関数なしでこの問題を解決している言語は特に思い当たらない

  • Zigの文法は散漫に感じる。@TypeOf のような @ で始まる構文や、.{.x} のような初期化文法がぎこちなく感じられる。Zigの使用にまだ慣れていないからかもしれないが、全体としてコードを読みにくい印象がある

    • Odinの文法はもっとミニマルでよく磨かれているので好みだ。Zigはやや散漫な感じがする

    • . はZigでは推論型のためのプレースホルダーとして機能する。たとえば次のようにオブジェクトを初期化できる

      const p = Point{ .x = 123, .y = 234 };
      

      あるいは型推論を明示したいなら

      const p: Point = .{ .x = 123, .y = 234 };
      

      関数引数でも型を省略できるので、より簡潔になる。Rustではこうした状況で明示的に型を書かなければならない

      takePoint(Point{ x: 123, y: 234 });
      

      ネストした構造体の初期化でも、Zigの推論方式ははるかに有用だ。どこでも明示的に型を書かなければならないRustは、すぐにコードが散漫になりがちだ。それでも先頭のdot記法は省いたほうが便利だと思うが、パーサ実装の単純化のために維持しているのだろう。x: 123.x = 123 という表記法は、それぞれJSとC99から借用したものだ。個人的にはどちらもよく使うので、違和感はないと思う

  • C# 11のraw string literal方式のほうがずっと好みだ。最初の行のインデントを基準に残りの行のインデントを自動調整してくれる。また、中括弧を文字として使うこともできる。$が複数回出てくると中括弧を完全に値として扱う

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (C# raw string literal機能の作者として)実際には最後の """ 行のインデントが基準で、最初の行もインデントできるようになっている。この機能を気に入ってもらえてうれしいし、良い機能だと自負している
  • Zigの文法も良いが、Goのようにセミコロンや : なしでも十分きれいに書けることを考えると、「lovely」とまでは思わない。比較するならRustよりはかなり改善されているのは確かだが、Goも十分優秀だ

    • むしろGoのように過度にミニマルな文法は、読むときに解釈しづらい場合がある。コードを書く時間より読む時間のほうが長いので、必要以上の簡潔さはかえってミスを招き、デバッグを難しくする。CoffeeScriptやJのような極端に省略された文法がその代表例だ

    • 文法要素を減らせば文法が良くなるわけではないと思う。もしそうなら皆Lispのように書くだろうし、scriptio continua(空白のない古代の筆記方式)のように文章も書いていただろう。scriptio continuaのWikipedia 参照

  • Zigには全体として満足しているが、次の問題は惜しい

    • ブロックの返り値を指定しにくい。Rustのように最後の式を自動で返り値として認識してくれるとよいが、Zigではlabelなどを使わなければならず面倒だ
    • オプショナル型のチェイニング(例: a?.b?.c)ができない。モナディック型のサポートがあればもっと一般的なチェイニングができるはずだが、まだ足りない
    • ラムダ関数のサポートがない。すでにループやcatchブロックのような場所で関数ブロックを使えるのだから、ラムダまでサポートされればさらに柔軟になると思う
  • 型名にvoidを使うことについては、実際にはvoidは型理論では「unit」の役割ではなく、値を持たない「uninhabited」型を意味する。伝統的には ()unit が1つのメンバーを持つ型だ。voidはabortのような関数の戻り値型だ

    • CやC++ではvoidがそれなりにうまく使われていて、多くのシステムプログラマにとって馴染み深い。型理論上の用語論争は実用上は無意味だと思う。Zigに来る人の多くはCやC++の背景を持っているので、voidで十分問題ない

    • abortはRustの ! 型のような「到達不能」状態のための型だ。voidはむしろunitや () に近く、値が存在しない型だ。面白いトリックとして、TypeScriptではvoidをジェネリック制約に使うと、そのパラメータをオプショナルにできる

    • void型には非常に長い伝統があり、ALGOL 68までさかのぼる。そこではVOID型は1つのメンバー(EMPTY)だけを持つ型として定義されている

  • 「Zigにはラムダがない」という点には驚いた。C++ではラムダをほぼ至る所で使うので、では配列のsortなどでcomparatorをどう定義するのか気になる

    • 普通は関数宣言を別にするので、その点ではZigは不便だと思う

    • 無名構造体とそこに含まれる関数をインラインで参照できる。実際、ラムダでよく使うキャプチャ機能はZigにはないが、コンテキストパラメータ(たいてい構造体)を渡す方式で代替できる

    • 基本的にはCと同じく、個別に関数を宣言してそのポインタをソート関数に渡す方式だ

  • 「文法は重要ではない」と言う人も、実際には「文法は重要ではないから自分の好みの方式で書こう」と言っているようなものだ。私もRust/Zig/GoのようなC系から派生した文法に慣れていて、Haskell/OCamlのように空白で関数呼び出しを区切るスタイルはまだ馴染みが薄く、普及の妨げになると思う。Rustの成功のように、関数型プログラミングという「ほうれん草」をシステム言語という「ブラウニー」にうまく練り込んだ点は、他の言語も参考にしてよいかもしれない

    • 文法が重要ではないという意見には同意しない。結局のところ、文法はユーザーが言語とやり取りするための主要なインターフェースだ。どの言語を読むときでも、文法要素は無意識のうちにより強く目に入ってくる

    • C系文法を持つ関数型言語が欲しいならGleamを勧める: gleam.run コードもとてもきれいだ

      fn spawn_greeter(i: Int) {
       process.spawn(fn() {
        let n = int.to_string(i)
        io.println("Hello from "  n)
       })
      }
      

      Reasonも勧められる。OCamlベースだがC系文法を持っている: reasonml.github.io