世界を動かす技術を、日本語で。

ジグの素敵な構文

概要

  • Zigの構文は シンプル かつ 洗練 された設計
  • RustやCなどの言語から 良い部分を継承 しつつ、独自の改良を加えている
  • 整数リテラル生文字列 など、特有の直感的な記法
  • 型宣言、関数宣言、制御構文 も分かりやすく統一
  • 名前解決やスコープ管理 も明確で、可読性と保守性を重視

Zigの魅力的な構文

  • Zigは 中括弧系言語 の中で、構文設計が非常に洗練されている言語
  • Rust から多くを借用しつつ、よりシンプルな意味論によって改善
  • 型推論ではなく、 暗黙のcomptime型変換 を採用
    • 例: const an_integer = 92; → 型は comptime_int
  • 整数リテラル にはサフィックスが不要
    • 型変換は代入時や明示的な型指定で行う
  • var x = 92; のような記法は基本的に不可、明示的な型指定が必要

生文字列リテラル

  • Zigの生文字列は \\ で始まり、各行が独立したトークン
    • エスケープの必要がなく、 インデント問題も回避
  • Rustの r##""## 記法に比べて、 可読性・保守性が高い
  • 行コメントのみ の仕様により、字句解析もシンプル

レコードリテラル

  • C風の構文を採用
    • 例: const p: Point = .{ .x = 1, .y = 2, }
  • .x = 1 の記法により、 フィールド書き込み箇所のgrepが容易
  • データフロー解析 の効率化

型宣言とポインタ

  • すべての型は 接頭辞型記法 (prefix)
    • 例: u32, [3]u32, *[3]u32
  • ポインタのデリファレンスは 後置 で記述
    • 例: ptr.* = 92;
  • Cの スパイラルルール の混乱を解消

識別子

  • 生識別子@"名前にスペース" のように記述
    • キーワードとの衝突回避や、特殊なエクスポート名に便利
  • Zigの組み込み構文(例: @TypeOf)や文字列と統一感

関数宣言

  • fn foo 記法で宣言
    • C/Java系より 検索性・可読性が高い
  • 戻り値型は 矢印(->)なし で記述
    • 例: fn add(x: i32, y: i32) i32
  • ラムダがない ため、戻り値型は常に必須

ローカル変数

  • constvarでバインディング
    • 例: const mid = lo + @divFloor(hi - lo, 2);
  • Rustのconstとは異なり、Zigのconstcomptimeではない
  • 型注釈は 'name' (':' Type)? の記法
    • 視認性と機械解析性が向上

制御フローと論理演算子

  • and, orキーワード として使用
    • 例: while (count > 0 and ascii.isWhitespace(...))
  • 短絡評価 が制御フローであることを明示
  • ビット演算は従来通り&, |を使用

明示的なreturn

  • return文は必須
    • 例: fn add(x: i32, y: i32) i32 { return x + y; }
  • ブロック式の値はvoid
    • セミコロン問題の回避、構文の一貫性

if式と三項演算子

  • 括弧は必須、波括弧は任意
    • 例: .direction = if (prng.boolean()) .ascending else .descending,
  • フォーマッタが バグを誘発する書式ミスを検出

ループ

  • Python同様、ループにelse句を付与可能
  • ループ自体が として使える
    • 例: pub const Word = for (.{ u8, u16, ... }) |W| { ... } else unreachable;
  • 無限ループ は明示的な条件で書く
    • 安全性のため、上限を設けてpanicを利用

イテレータ構文

  • for (slice) |element| { ... } のように、 コレクションが先、変数が後
  • 直感的で読みやすい

名前の明確性とスコープ

  • 変数のシャドウイング禁止
    • バグの温床を排除
  • 名前解決の複雑性を徹底排除
    • プレリュードやグロブインポートなし
    • 標準ライブラリ利用時も明示的にインポート
  • 継承、ミックスイン、暗黙的なトレイト等なし
    • x.foo()は必ずx型に定義されたメソッド
  • メソッドとフィールドの同名禁止
  • 名前空間なし で、スコープ内の名前衝突を回避
    • foo.bar.baz 記法で十分

Zigの構文設計は、 簡潔さ・明快さ・保守性 を最重視し、他言語の課題を巧みに解決している。 可読性・機械解析性・安全性 を追求する開発者にとって、Zigは非常に魅力的な選択肢。

Hackerたちの意見

Zigの構文はちょっと騒がしいと思う。@TypeOf(アットマーク)や仲間たちが好きじゃないし、.{.x}の変な構文もなんか違和感がある。Zigにはいいところもあるけど、コードがすごく読みづらいのは認めるよ。自分がZigにあまり詳しくないせいだと思う。

ドットは推論された型のプレースホルダーに過ぎないと思うけど、それはすごく理にかなってるよ。例えば、これを書ける:const p = Point{ .x = 123, .y = 234 }; それともこれ:const p: Point = .{ .x = 123, .y = 234 }; Pointを期待する関数を呼ぶ時は、冗長な型を省略できる:takePoint(.{ .x = 123, .y = 234 }); Rustでは型を明示的に書かなきゃいけないから、takePoint(Point{ x: 123, y: 234 }); みたいにね。ネストした構造体の初期化では、推論形式がすごく便利なんだ。Rustではこう書かなきゃいけない(文法が合ってるかは分からないけど):const x = Rect{ top_left: Point{ x: 123, y: 234 }, bottom_right: Point{ x: 456, y: 456 }, }; でもコンパイラはRectが二つのネストしたPointから成り立ってることを知ってるから、わざわざユーザーにそれを書かせる意味があるの?だからZigではこう書ける:const x = Rect{ .top_left = .{ .x = 123, .y = 234 }, .bottom_right = .{ .x = 456, .y = 456 }, }; すべてに明示的な型を要求すると、Rustではすぐに煩雑になっちゃうよね。もちろん、'.{'の先頭のドットを省略できるかどうかが問題なんだけど、個人的には省略した方がいいと思う。どうやらパーサーを簡素化するらしいけど、そんな実装の詳細が便利さの妨げになるべきじゃないと思う。それに、.x = 123x: 123の違いもある。Zigの形式はC99からコピーされたもので、Rustの形式はJavascriptから来てる。自分はC99とTypescriptをよく書くから、どちらの形式も好きなんだ(残念ながらZigとRustはC99の指定初期化構文の柔軟性や便利さには全然及ばないけど)。追記:Rustの構造体初期化の文法を修正した。

Zig はうるさいし、構文もあんまり洗練されてない。オーディンの構文が好きな理由の一つは、ミニマルでよく考えられてるからなんだよね。

「Zigにはラムダがない」って言われて驚いた(C++使いとして)。ラムダはどこでも使ってるから、Zigで配列をソートする時の比較関数を定義する標準的な方法は何なの?

Cと同じように、名前付き関数を定義して、ソート関数へのポインタを渡す。

普通の関数宣言だね。これがZigを柔軟性のないものにしているポイントだと思う。

無名構造体を宣言して、その中に関数を持たせてインラインでその関数を参照することもできるよ(やりたいならね)。専用の言語機能よりは少し構文が多いけど、そんなに多くはない。Zigに足りないのは、ラムダ実装が通常持っているキャプチャ機能だね。Zigでは通常、コンテキストパラメータでそれを実現するんだけど、また構造体を使うことが多いんだ。

Rustと同様に、Zigは型を指定するために'name'(':' Type)?構文を使うけど、これはType 'name'よりもいいと思う。これは明らかに少数派の意見だと思うけど、逆に好みが違うんだ。変数宣言を確認する最も一般的な理由は、その変数の型を特定するためで、視覚的にそれを見つけるのが難しいほど、イライラするんだ。特に静的型付けの言語では、「これはintだ」というメンタルモデルになることが多いから、「これはたまたま'int'型の変数だ」という考え方にはならない。特にRustでは、可変変数がlet mutで宣言されるから、すべての宣言でletを使わなきゃいけなくて、ちょっと冗長な構文になってる。CやC++では、その不必要なletの代わりに型が入るはずだし、C(C23以降)でもautoキーワードで型推論ができる。自分は、型を知る必要がない場所ではオプショナルな型推論を使い、型を指定する必要がある時には、それが読み返す時に役立つコメントとして機能するようにしてる。

CやC++では、その不必要なletの代わりに型が入る 自分はそのletはあまり不必要じゃないと思う。変数を宣言する文を明確に示すから、関数呼び出しのような他の文とは区別できるんだ。これがC++が「最も厄介な解析」の曖昧さを解決する必要がある理由でもある。

同じような感じだね。最初に型が来ると、頭の中で理解するのが早いんだ。物の名前よりも型の方が重要だし(でも名前も大事!)、だから型を名前の前に持ってきたい。パーサーの観点から見ると、名前を先に持ってくる方がASTに追加しやすくて、型判定器に渡して宣言を完了させるのも簡単だからね。だからその気持ちわかるよ。TypeScriptではそうなってるから、パーサーが型を完全に無視してJavaScriptと互換性を持たせることができるんだ(でも型を外すのは簡単だし、なんでそんなことするの?)。Goはさらにクレイジーな規約があるよね。大文字と小文字で公開と非公開の区別、継承なし、パフォーマンス重視の開発者を避けるGC。結局、使いやすい標準ライブラリがあればいいんだ。アプリを作るためにね。型を使ってあれこれするのはもううんざり。type Add = [ …Array, …Array, ][“length”]みたいな型システムの乱用には本当にイライラする。賢くなる必要はない、ただ関数をエクスポートすればいいんだ。すべての状態を表す型なんていらない、意図が必要なんだ。

たぶん、慣れの問題だと思う。F#からRustに移ったとき、同じname : typelet mutable nameがあったから、好きになった要因の一つだったんだ。

変数宣言を確認する最も一般的な理由は、その変数の型を知るためだね。マウスカーソルを上に乗せれば、どんな合理的なエディタでも型を表示してくれるよ。 > 特にRustでは、可変変数はlet mutで宣言されるから、毎回letを使うことになって、ちょっと冗長になるんだ。Rustは奇妙な実装理由で非常に冗長なんだよ... つまり、解析のあいまいさを避けるために。 > CやC++では、その不要なletの代わりに型が来るんだよね。一方で、それだと「foo」という名前の変数や関数の宣言を信頼性を持ってgrepできない。自動的にfoo(int blah) -> boolスタイルを使うのが好きな人もいる理由を考えてみて。これはテンプレートのナンセンス(型パラメータがわかる前に戻り値の型を宣言する方法)から導入されたけど、すごく理にかなってるし、コードをgrepしやすくするんだ。ジェネリック型パラメータがあると、戻り値の型を前に持ってくるのがすごく変になるよね -- 読みの順番的に。とにかく...

Hacker Newsで議論の続きを見る