- Zig, Rust जैसे curly-brace आधारित syntax पर बना है, लेकिन अधिक सरल language semantics और बेहतर syntax choices के साथ इसे और निखारता है
- Integer literals में सभी types
comptime_int से शुरू होते हैं और assignment के समय स्पष्ट रूप से convert किए जाते हैं, जबकि string literals \\ आधारित संक्षिप्त raw string notation का उपयोग करते हैं
.x = 1 रूप के record literals field writes को आसानी से searchable बनाते हैं, और सभी types को prefix notation में एकसमान तरीके से व्यक्त किया जाता है
and·or को control-flow keywords की तरह इस्तेमाल किया जाता है, और if·loop syntax में वैकल्पिक रूप से curly braces छोड़ी जा सकती हैं, जबकि formatter safety सुनिश्चित करता है
- Namespace के बिना हर चीज़ को expression की तरह संभाला जाता है, जिससे type·value·pattern syntax एकीकृत हो जाती है, और generics·record literals·built-in functions (
@import, @as आदि) को संक्षेप में इस्तेमाल किया जा सकता है
अवलोकन
- Zig का बाहरी रूप Rust से मिलता-जुलता है, लेकिन यह अधिक सरल language structure अपनाता है
- Syntax design में grep-friendliness, syntactic consistency, और अनावश्यक visual noise को कम करने पर ध्यान है
Integer literals
const an_integer = 92;
assert(@TypeOf(an_integer) == comptime_int);
const x: i32 = 92;
const y = @as(i32, 92);
- सभी integer literals का type
comptime_int होता है
- Variable में assign करते समय type को स्पष्ट रूप से लिखना होता है या
@as से convert करना होता है
var x = 92; रूप काम नहीं करता, इसमें explicit type चाहिए
String literals
const raw =
\\Roses are red
\\ Violets are blue,
\\Sugar is sweet
\\ And so are you.
\\
;
- हर पंक्ति एक अलग token होती है, इसलिए indentation की समस्या नहीं होती
\\ को खुद escape करने की ज़रूरत नहीं होती
Record literals
const p: Point = .{
.x = 1,
.y = 2,
};
.x = 1 फ़ॉर्मेट पढ़ने/लिखने के अंतर को समझने में मददगार है
.{} notation block से अलग पहचानी जाती है और result type में अपने-आप convert हो जाती है
Type notation
u32 // 정수
[3]u32 // 길이 3 배열
?[3]u32 // null 가능 배열
*const ?[3]u32 // 상수 포인터
- सभी types prefix notation में लिखे जाते हैं
- Dereference suffix notation (
ptr.*) में होता है
Identifier
const @"a name with space" = 42;
- Keyword conflict से बचा जा सकता है या विशेष नाम दिए जा सकते हैं
Function declaration
pub fn main() void {}
fn add(x: i32, y: i32) i32 {
return x + y;
}
fn keyword और function name साथ-साथ होने से search करना आसान होता है
- Return type notation में
-> का उपयोग नहीं होता
Variable declaration
const mid = lo + @divFloor(hi - lo, 2);
var count: u32 = 0;
const और var का उपयोग होता है
- Type notation
नाम: type क्रम में होती है
Control flow: and/or
while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {
count -= 1;
}
and, or control-flow keywords हैं
- Bit operations के लिए
&, | का उपयोग होता है
if statement
.direction = if (prng.boolean()) .ascending else .descending;
- Parentheses अनिवार्य हैं, curly braces वैकल्पिक
zig fmt सुरक्षित formatting सुनिश्चित करता है
Loops
for (0..10) |i| {
print("{d}\n", .{i});
} else @panic("loop safety counter exceeded");
for, while दोनों else clause को support करते हैं
- Iterator और element names को सहज तरीके से रखा जाता है
Namespace और name resolution
const std = @import("std");
const ArrayList = std.ArrayList;
- Variable shadowing की अनुमति नहीं है
- Namespace और glob import नहीं हैं
हर चीज़ expression है
const E = enum { a, b };
const e: if (true) E else void = .a;
- Type·value·pattern syntax को एकीकृत किया गया है
- Type position में conditional expression रखी जा सकती है
Generics
fn ArrayListType(comptime T: type) type {
return struct {
fn init() void {}
};
}
var xs: ArrayListType(u32) = .init();
- Generics को function-call syntax (
Type(T)) से व्यक्त किया जाता है
- Type arguments हमेशा explicit होते हैं
Built-in functions
const foo = @import("./foo.zig");
const num = @as(i32, 92);
@ prefix से compiler-provided features को call किया जाता है
@import file path को स्पष्ट रूप से दिखाता है
- Arguments अनिवार्य रूप से string literal होने चाहिए
निष्कर्ष
- Zig का syntax छोटे-छोटे चुनावों का ऐसा समूह है जो मिलकर पढ़ने में आसान भाषा बनाता है
- Features की संख्या घटाने से ज़रूरी syntax भी कम होती है, और syntax के बीच टकराव की संभावना भी घटती है
- मौजूदा भाषाओं के अच्छे विचार अपनाते हुए, ज़रूरत पड़ने पर नई syntax को भी साहसपूर्वक शामिल किया जाता है
1 टिप्पणियां
Hacker News की राय
यह लेख grammar design में आने वाले कई trade-offs को गहराई से कवर करता है, और Zig की grammar का minimalism, consistency, और लगभग बेरहमी की हद तक readability पर उसका फ़ोकस सचमुच प्रभावशाली लगा। यह कोई अमूर्त सुंदरता नहीं है, बल्कि industrial use में बिना चौंकाने वाली 'brutalism' जैसी चीज़ है, और यही बात मुझे पसंद आई। इस तरह की संतुलित grammar design वाकई दुर्लभ है, और मुझे लगता है Zig ने इसे बहुत अच्छी तरह किया है
अफ़सोस है कि article में error handling का ज़िक्र नहीं था। Zig का try/catch तरीका बहुत शानदार है, और कई भाषाओं में यह मेरा पसंदीदा error handling approach है। अगर यह हिस्सा भी शामिल होता तो और अच्छा होता
Zig की असली खूबसूरती 'ऊपरी तौर पर सुंदर readability' में नहीं, बल्कि abstraction से मिलने वाली consistent सुंदरता में है। S-expression और M-expression की तुलना की तरह, सामान्य case के लिए अच्छा approach लंबे समय में अक्सर कई exceptional situations के लिए special design से बेहतर साबित होता है। C++ की तरह हर तरह के exception case जोड़ते रहने पर आखिरकार सभी rules याद रखने का बोझ बढ़ जाता है। language design में simplicity और consistency का पीछा करते-करते कभी-कभी जटिलता user पर छोड़ दी जाती है और वह 'Turing tarpit' बन सकती है, इसलिए ऐसा approach महत्वपूर्ण है जहाँ special cases सामान्य rules से स्वाभाविक रूप से हल हो जाएँ। XKCD की New Pet comic में भी इसका एक उदाहरण देखा जा सकता है
अगर कोई खास उदाहरण आपको प्रभावशाली लगा हो, तो क्या आप उसे साझा कर सकते हैं?
Zig में Rust की तरह 'name:type' फ़ॉर्मेट से type annotation करने की बात पर, मुझे उल्टा traditional style ज़्यादा पसंद है जहाँ type पहले आता है। किसी variable declaration को दोबारा देखते समय सबसे पहले मैं उसका type जानना चाहता हूँ, और अगर वह जल्दी न मिले तो असुविधा होती है। खासकर Rust में
let mutजैसे अनावश्यक रूप से दोहराए गए हिस्से हैं, जिससे यह और झंझटपूर्ण लगता है, और C, C++ की तरह type पहले आने वाला तरीका भी अच्छा है। व्यवहार में मेरा मानना है कि type inference सिर्फ वहीं न्यूनतम रूप से इस्तेमाल होना चाहिए जहाँ उसकी ज़रूरत होletkeyword का एक काम यह भी है कि वह साफ़ करता है कि यह declaration statement है। वरना C++ की ambiguous syntax parsing जैसी समस्याएँ झेलनी पड़ सकती हैंमैं भी हमेशा पहले variable type देखने की कोशिश करता हूँ, इसलिए type-first style पसंद करता हूँ। parser के नज़रिए से name को पहले process करना सुविधाजनक होता है, और TypeScript ने JavaScript compatibility की वजह से यह structure अपनाया है, यह बात समझ में आती है। अंततः मुझे लगता है कि असली चीज़ आसान standard library है। type system के अतिशय दुरुपयोग वाले उदाहरणों की तरह, हर state को type में ठूँसने से ज़्यादा महत्वपूर्ण है intent को साफ़ तौर पर व्यक्त करना
मैं code में किसी variable का type देखने के लिए ऊपर लौटता हूँ, लेकिन उल्टा type पहले हो तो जिस variable declaration को ढूँढ रहा हूँ उसे खोजना और कठिन हो जाता है। type name सबसे आगे होता है और उसकी लंबाई बदलती रहती है, इसलिए नज़र को बार-बार दाएँ-बाएँ ले जाना पड़ता है और यह अक्षम लगता है
ज़्यादातर मामलों में editor में hover करने पर type information तुरंत दिख जाती है, इसलिए code में type की position उतनी महत्वपूर्ण नहीं भी हो सकती। Rust का verbose होना parsing ambiguity से बचने का काफी implementation-related कारण है। C, C++ की तरह type पहले हो तो किसी खास नाम से declare किए गए variable को grep से आसानी से ढूँढना मुश्किल हो जाता है, और return type को आगे रखने वाली style templates की वजह से आई थी, लेकिन कुछ मामलों में इससे code पढ़ना और ढूँढना आसान भी हो जाता है
निजी तौर पर मुझे Pascal-style type annotation ज़्यादा पसंद है। type inference होने पर भी किसी अलग 'auto' जैसे workaround की ज़रूरत नहीं पड़ती, और parsing के नज़रिए से भी यह कम ambiguous है।
MyClass xमें तुरंत यह समझना मुश्किल होता है कि MyClass type है या variable name, इसलिए यह ambiguity कम करता हैZig की raw/multiline string grammar में
\\को कई बार लिखना बहुत उलझाऊ और चरम लगता हैअगर आपने Python, C++, Rust आदि में multiline strings को format किया है, तो उसकी असुविधा समझेंगे। indentation के string content में शामिल हो जाने की समस्या हमेशा रहती है, और YAML की तरह indentation stripping mode वाले cases कभी-कभी उल्टा और confusion बढ़ाते हैं। Zig का तरीका indentation के मामले में बहुत स्पष्ट है
शुरुआत में मुझे भी यह syntax बहुत असुविधाजनक लगा था, लेकिन Zig इस्तेमाल करते-करते इसकी आदत हो जाती है और इसके फायदे दिखने लगते हैं। Zig में अक्सर ऐसा होता है कि पहली नज़र में चीज़ें पसंद न आएँ, लेकिन वास्तव में इस्तेमाल करने पर उनकी उपयोगिता समझ आती है
सच कहें तो यह कोई पागल syntax नहीं, बल्कि एक पागलपन भरी समस्या है—multiline string के अंदर दूसरी multiline string को सुरक्षित ढंग से डालने की समस्या। Zig में अलग escaping की ज़रूरत नहीं पड़ती और indentation की चिंता भी नहीं करनी होती, यह अच्छी बात है
Kotlin का trimIndent, Go या Java के text blocks, और खास तौर पर Go का backtick raw string तरीका मुझे अधिक smooth लगता है। Zig में
\\की वजह से मैं उल्टा@embedFileवाला रास्ता इस्तेमाल करता हूँvisual तौर पर
\\मुझे पसंद नहीं है, लेकिन मुझे लगता है कि यह multiline literals और indentation की समस्या का साफ़ समाधान है। functions के बिना यह समस्या हल करने वाली कोई language मुझे खास याद नहीं आतीZig की grammar बिखरी हुई लगती है।
@TypeOfजैसी@से शुरू होने वाली syntax या.{.x}जैसी initialization syntax अटपटी लगती है। हो सकता है कि ऐसा इसलिए लगे क्योंकि मैं Zig में दक्ष नहीं हूँ, लेकिन कुल मिलाकर code पढ़ना मुश्किल लगता हैOdin की grammar कहीं ज़्यादा minimal और polished है, इसलिए वह पसंद है। Zig थोड़ा बिखरा हुआ महसूस होता है
.Zig में inferred type के लिए placeholder की तरह काम करता है। उदाहरण के लिए, object को ऐसे initialize किया जा सकता हैया अगर आप type inference को स्पष्ट करना चाहें तो
function arguments में भी type छोड़ा जा सकता है, इसलिए यह और concise हो जाता है। Rust में ऐसी स्थिति में type को explicitly लिखना पड़ता है
nested struct initialization में भी Zig का inference approach काफी उपयोगी है। Rust में हर जगह explicitly type लिखना पड़े तो code जल्दी बिखरा हुआ लग सकता है। फिर भी मुझे लगता है कि leading dot notation हटाना ज़्यादा सुविधाजनक होता, लेकिन parser implementation को सरल रखने के लिए शायद इसे बनाए रखा गया है।
x: 123या.x = 123notation क्रमशः JS और C99 से लिया गया है। निजी तौर पर मैं दोनों का काफ़ी इस्तेमाल करता हूँ, इसलिए ये अटपटे नहीं लगतेC# 11 का raw string literal तरीका कहीं ज़्यादा पसंद है। पहली line की indentation को आधार बनाकर बाकी lines की indentation अपने-आप match की जाती है। साथ ही braces को literal character की तरह भी इस्तेमाल किया जा सकता है। अगर
$कई बार आए तो braces पूरी तरह value की तरह treat होते हैं"""line की indentation होती है, और पहली line को भी indent किया जा सकता है। यह जानकर खुशी हुई कि आपको यह feature पसंद आया, और मुझे भी इस पर गर्व है कि यह एक अच्छा feature हैZig की grammar अच्छी है, लेकिन Go की तरह semicolon या ':' के बिना भी चीज़ें काफ़ी साफ़-सुथरी लिखी जा सकती हैं, इसलिए मैं इसे 'lovely' तक नहीं कहूँगा। तुलना करनी हो तो Rust से यह निश्चित रूप से बहुत बेहतर है, लेकिन Go भी काफ़ी उत्कृष्ट है
उल्टा, Go जैसी अत्यधिक minimal grammar को पढ़ते समय कुछ मामलों में interpret करना और कठिन हो सकता है। code लिखने से ज़्यादा समय उसे पढ़ने में जाता है, इसलिए ज़रूरत से ज़्यादा conciseness उल्टा गलतियों को जन्म दे सकती है और debugging को कठिन बना सकती है। CoffeeScript और J जैसी अत्यधिक संक्षिप्त grammar इसके प्रतिनिधि उदाहरण हैं
मुझे नहीं लगता कि grammar elements हटा देने भर से grammar बेहतर हो जाती है। अगर ऐसा होता, तो सब Lisp की तरह लिखते, और scriptio continua (बिना spaces की प्राचीन लेखन शैली) में भी लिखते। scriptio continua Wikipedia देखें
कुल मिलाकर Zig संतोषजनक है, लेकिन ये समस्याएँ खटकती हैं
a?.b?.c) संभव नहीं है। monadic types का support हो तो ज़्यादा सामान्य chaining संभव हो सकती थी, लेकिन अभी कमी हैtype naming में
voidइस्तेमाल करने पर, type theory के हिसाब सेvoidअसल में 'unit' की भूमिका नहीं बल्कि बिना किसी value वाला 'uninhabited' type होता है। पारंपरिक रूप से()याunitएक member वाला type माना जाता है।voidतोabortजैसी function का return type हैC, C++ में
voidइतना लंबे समय से ठीक-ठाक इस्तेमाल हो रहा है कि बहुत से systems programmers के लिए यह परिचित है। formal type theory की terminology पर बहस व्यावहारिक उपयोग में अर्थहीन लगती है। Zig में आने वाले बहुत से लोग C, C++ background से आते हैं, इसलिएvoidबिल्कुल ठीक हैabortका type तो Rust के!type की तरह 'unreachable' स्थिति के लिए होना चाहिए।voidज़्यादा से ज़्यादाunitया()के करीब है, यानी ऐसा type जिसमें कोई सार्थक value नहीं होती। एक दिलचस्प trick यह है कि TypeScript मेंvoidको generic constraint में इस्तेमाल करें तो संबंधित parameter को optional बनाया जा सकता हैvoidtype की बहुत पुरानी परंपरा है, जो ALGOL 68 तक जाती है। वहाँVOIDtype को एक member (EMPTY) वाले type के रूप में परिभाषित किया गया था"Zig में lambda नहीं हैं" यह बात चौंकाने वाली है। C++ में तो lambdas लगभग हर जगह इस्तेमाल होते हैं, तो फिर array sort जैसी जगहों पर comparator कैसे define करते हैं, यह जानने की जिज्ञासा है
आम तौर पर अलग से function declaration करनी पड़ती है, और इस लिहाज़ से Zig उस हिस्से में असुविधाजनक लगता है
anonymous structs और उनमें शामिल functions को inline refer किया जा सकता है। असल में lambdas में सबसे ज़्यादा इस्तेमाल होने वाला capture feature Zig में नहीं है, लेकिन context parameter (आमतौर पर struct) पास करके उसका कुछ हद तक विकल्प किया जा सकता है
मूल रूप से C की तरह ही, आप अलग function declare करते हैं और उसका pointer sort function को दे देते हैं
लोग कहते हैं "grammar महत्वपूर्ण नहीं है", लेकिन व्यवहार में मतलब अक्सर यह होता है कि "grammar महत्वपूर्ण नहीं है, इसलिए हम वही इस्तेमाल करेंगे जो मुझे पसंद है।" मैं भी Rust/Zig/Go जैसी C-family derived syntax से परिचित हूँ, और Haskell/OCaml की तरह whitespace से function call अलग करने वाली style अब भी अनजानी लगती है, इसलिए मुझे लगता है कि वह mainstream adoption में बाधा बनती है। Rust की सफलता की तरह, functional programming की 'पालक' को systems language की 'brownie' में अच्छे से मिला देने वाली बात दूसरी भाषाएँ भी सीख सकती हैं
मैं इस बात से सहमत नहीं हूँ कि grammar महत्वपूर्ण नहीं है। आखिरकार grammar वही main interface है जिससे user language के साथ interact करता है। किसी भी language को पढ़ते समय syntax elements अनजाने में और ज़्यादा उभरकर सामने आते हैं
अगर आप C-like syntax वाली functional language चाहते हैं, तो Gleam की सिफारिश करूँगा: gleam.run इसका code भी बहुत सुंदर है
Reason भी सुझाने लायक है। यह OCaml पर आधारित है, लेकिन इसकी syntax C-family जैसी है: reasonml.github.io