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

ジグの素敵な構文

概要

  • 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しやすくするんだ。ジェネリック型パラメータがあると、戻り値の型を前に持ってくるのがすごく変になるよね -- 読みの順番的に。とにかく...

俺も変数の型を見つけるためにコードを上に戻ることがあるよ。でも逆の問題もあると思うんだ。型が最初だと、興味のある変数を宣言している行を見つけるのが難しくなる。変数名が最初じゃないからね。型の後に変数名が来るんだけど、その型が「int」か「struct Frobnosticator」か分からない。つまり、変数名が可変長の型の後に来るから、変数名のスタートを見つけるために左から右に行ったり来たりしなきゃいけないんだ。

Pascalの型付けが好きなんだ。1. C++のようなハッキーな「auto」回避なしで型推論ができるし、2. パースがあまり曖昧じゃないから。つまり、「MyClass x」を読むとき、MyClassは変数(この場合はエラー)か型か分からないけど、文脈がないと判断できないんだ!

Zigには素敵なベクトルやクォータニオン、行列の構文があればいいのに。チームが演算子オーバーロードを追加しないっていうのが、これを妨げてるね。

ベクトルや行列の数学にオペレーターのオーバーロードは必要ないよ。ほとんどのGPU言語を見ればわかる。Zigに足りないのは、Clangの拡張ベクトルと行列の完全なマッピングだね(かなり制限された@Vector型じゃなくて): https://clang.llvm.org/docs/LanguageExtensions.html#vectors-... https://clang.llvm.org/docs/LanguageExtensions.html#matrix-t...

同意だね、彼らがそれを許可しない理由は変だよね。隠れたオーバーロードはない?じゃあ、はっきりさせようよ:#+、#/ でいいじゃん。

自分もZigは好きだけど、その構文が素敵だとは思わない。Goは行の区切りに;を使わず、変数の型に:を使わなくても結構うまくやれるって示してるよね。でも、Rustと比べると大きな改善だとは思う。

個人的には、Goのシンプルな構文は読むときに解析が難しいと感じる。コードを読む時間の方が、書く時間よりも多いからね(書いてる時でさえ)。過度に簡潔な構文は、タイプミスに気づかれないと、全く予想外の正しい構文のプログラムになったり、もっと後で謎のエラーとして現れたりするから、かなり厳しいよ。具体例を挙げると、CoffeeScriptやJみたいな感じ。

何かを削除することが必ずしも構文を良くするわけじゃないよ。そうじゃなきゃ、みんなLispを使って、スペースバーなんて存在しないはずだしね。 https://en.wikipedia.org/wiki/Scriptio_continua

生の文字列や複数行の文字列はこう書く: const still_raw = \const raw = \ \バラは赤い \ \スミレは青い、\ \砂糖は甘い \ \君もそうだよ。 \ \ \; \ ; この構文はかなり狂ってると思う。

伝統的なマルチライン文字列(例えばPython、C++、Rustなど)をフォーマットしたことがないなら、そう思うかもね。 明らかでないなら、問題はインデントが文字列自体の一部になっちゃうから、ちゃんとインデントできないってこと。 いくつかの言語には、文字列のインデントを「削除」する魔法のモード(例えばYAML)があるけど、あんまり役に立たなくて混乱を招くだけだよ。この構文はかなり明確だと思う(少なくともインデントに関してはね。末尾の改行については、文字列はどこで終わるのか、ちょっと不明だけど)。

俺の最初の感想は「うーん、変だけど結構いいな」って感じだった。インデントの問題は確かに面倒だし、そこそこ良い構文ハイライターを使えば、//を目立たなくして、視覚的にごちゃごちゃしないようにできると思う。

Zigは本当にウィンドウショッパーにアピールしようとはしてないね。これはちょっと物議を醸す決定の一つで、実際に使ってみるとその良さが分かるようになる。最初に学んだときは構文が気に障った人間として言わせてもらうけど。

それは非常に合理的で、いくつかの技術的および認知的な利点があると思うよ。君がただ慣れてないから感情的に反応してるだけで、実際に悪いわけじゃないと思う。

みんな最初はこんな反応すると思うけど、使い始めるとすごく納得できるよね。特に、複数のカーソルを使えるエディタで選択操作をするときは。

見た目は\があまり好きじゃないけど、マルチラインリテラルとインデントの問題を便利で明確な方法で解決しているのはいいね。他の言語で関数なしにこの問題を解決しているものは知らないな。

KotlinはtrimIndentでうまく解決していると思う。Golangが好きだった気がするし、Javaは一番苦手だったけど、Javaもついにクリーンなテキストブロックのサポートを追加したみたい。埋め込まれたシェーダーコードやアセンブリ、JavaScriptを追加するのがすごく楽になって、読みやすくなったと思う。正規表現のようなものには、Golangのバックティック「生文字列」構文が好きだった。Zigでは、\の汚染を避けるために@embedFileを使っているよ。

これは狂った構文じゃなくて、解決するのがかなり狂った問題なんだ。通常、マルチライン文字列を別のマルチライン文字列の中で表現するには、たくさんの面倒なエスケープが必要になるんだよ。これがこの例のポイント:Zig ではエスケープもインデントの必要もないってこと。

各行にデリミタを繰り返すアイデアはいいと思う。ただ、//はコメントに見えるな。ダブルクォートを使うのもアリだと思うよ。例えば、こんな感じで:

const still_raw = "const raw = " "Roses are red " " Violets are blue, " "Sugar is sweet " " And so are you. " " ";

これなら文字列リテラルと混同することはないよ。だって、文字列リテラルには改行が含まれないからね。

これの構文ハイライトがあれば、もっと読みやすくなると思う。先頭の\\を文字列の内容とは違う色にするとか。

C# 11の生文字列リテラル、めっちゃ好きだな。最初の行のインデントを取って、その後の行も同じインデントだと仮定してくれるんだ。

string json = $""" {title} ようこそ {sitename} へ。 """;

しかも、埋め込まれた波括弧を実際の文字として使えるのもいいね。

string json = $$""" {{title}} ようこそ {{sitename}}、これは {sitename} 構文を使ってるよ。 """;

$が2回出てくることで、波括弧の補間が2つの波括弧に切り替わって、単一の波括弧はそのまま残るんだ。

ちょっとした訂正をさせてね(私はC#の生文字列リテラル機能の作者だから)。最後の """ 行のインデントが他の行から削除されるもので、最初の行のインデントではないんだ。これによって最初の行もインデントできるようになってる。ありがとう、気に入ってくれて嬉しいよ。あの機能は本当にいい仕事をしたと思ってる :-)

Zigは素晴らしいよ。書くのが楽しい。ただ、いくつか気になる点がある。

  • ブロックから値を返すのが難しい。Rustはブロックの最後の式の値をそのブロックの戻り値として扱うけど、Zigではラベルを使ってそれをするのが面倒なんだ。
  • オプショナルチェックをチェーンできない(例:a?.b?.c)。モナディック型のサポートがあれば、一般的なチェーン操作がサポートされるのに。
  • ラムダサポートがない。関数ブロックはすでにいくつかの場所でサポートされてるけど、例えばforループのブロックやcatchブロックとか。

この記事は本当に素晴らしいし、文法設計のトレードオフについて深い理解があるね。残念ながら、タイトルからの反応や表面的な文法の美しさに対する感情的な反応が多く見られる。Zigの文法で「素敵だ」と感じる点は、ミニマリズムと一貫性があって、読みやすさを徹底的に優先しているところだと思う。抽象的な思考をする人の心をくすぐるような「表面的に美しい」読みやすさではなく、工業用途において驚きの余地を残さないようなブルータリズムがある。こんなふうに文法設計をバランスよくするのは本当に難しいけど、Zigは素晴らしくて尊敬できる仕事をしているよ。

それは抽象的な思考をする人の心をくすぐるような「表面的に美しい」読みやすさではない というより、ここで目指している美しさは、少しの抽象を必要とするタイプの美しさなんだ。具体的な文法が視覚的に美しいというわけではなく、抽象的な文法を優雅に露わにしているから、具体的な文法よりも本質的に規則的で曖昧さがない。S式がM式に勝った理由も同じで、特別なケースが良いことが多いけど、特別なケースに合わせるための精神的負担がかかるから、一般的なケースから特別なケースを作る言語を探しているんだ。C++を見てみると、特別なケースがたくさんあって特定の構文を書くのが楽になるけど、その代償としてすべてを覚えておかないといけなくなる(テンプレートを使う場合は特に)。このトレードオフは言語設計ではよくあることで、一般的なケースの結果として特別なケースが良くなるような言語を探しているんだ。単にシンプルで一貫性があるだけだと、チューリングの泥沼に陥るからね。プログラマーにすべての複雑さを押し付けることになる。

記事について唯一不満なのは、エラーハンドリングに触れていないこと。笑 Zigのtry/catchの使い方は素晴らしくて、どの言語のエラーハンドリングよりもお気に入りだよ。この記事に入っていてもよかったと思う。

何か印象に残った例があれば教えてくれる?

型の名前として、() よりも void の方が好きかな。でも、名前が間違ってる。型理論では、()、つまり一つのメンバーを持つ型は「ユニット」と呼ばれ、"Void" は無居住型なんだ。Void は例えば abort の戻り値の型だよ。

abort の意味を正しく理解していれば、never か Rust の ! の型を持ってるはず。これは「制御フローがどこか別のところに行ったために取得できない値」という意味だよ。voidunit() に近い、許可されている値がない型だからね。面白いトリックとして、いくつかの言語(例えば TypeScript)は void ジェネリクスを許可して、そういう型のパラメータをオプショナルにできるんだ。

[削除済み]

C や C++ の void 型と同じ役割を果たしてるよね。ほとんどのシステムプログラマーは、純粋性についての型理論者の無駄話なんて気にしないと思う。Zig に来る人の多くは C や C++ から来るだろうし、void で全然問題ないよ。