7 पॉइंट द्वारा GN⁺ 2025-08-22 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 टिप्पणियां

 
GN⁺ 2025-08-22
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 सिर्फ वहीं न्यूनतम रूप से इस्तेमाल होना चाहिए जहाँ उसकी ज़रूरत हो

    • let keyword का एक काम यह भी है कि वह साफ़ करता है कि यह 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 किया जा सकता है

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

      या अगर आप type inference को स्पष्ट करना चाहें तो

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

      function arguments में भी type छोड़ा जा सकता है, इसलिए यह और concise हो जाता है। Rust में ऐसी स्थिति में type को explicitly लिखना पड़ता है

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

      nested struct initialization में भी Zig का inference approach काफी उपयोगी है। Rust में हर जगह explicitly type लिखना पड़े तो code जल्दी बिखरा हुआ लग सकता है। फिर भी मुझे लगता है कि leading dot notation हटाना ज़्यादा सुविधाजनक होता, लेकिन parser implementation को सरल रखने के लिए शायद इसे बनाए रखा गया है। x: 123 या .x = 123 notation क्रमशः JS और C99 से लिया गया है। निजी तौर पर मैं दोनों का काफ़ी इस्तेमाल करता हूँ, इसलिए ये अटपटे नहीं लगते

  • C# 11 का raw string literal तरीका कहीं ज़्यादा पसंद है। पहली line की indentation को आधार बनाकर बाकी lines की indentation अपने-आप match की जाती है। साथ ही braces को literal character की तरह भी इस्तेमाल किया जा सकता है। अगर $ कई बार आए तो braces पूरी तरह value की तरह treat होते हैं

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (C# raw string literal feature के लेखक के रूप में) वास्तव में आधार आख़िरी """ 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 संतोषजनक है, लेकिन ये समस्याएँ खटकती हैं

    • block के return value को तय करना कठिन है। Rust की तरह आख़िरी expression को अपने-आप return value मान लिया जाए तो अच्छा हो, लेकिन Zig में labels आदि इस्तेमाल करने पड़ते हैं, जो झंझटपूर्ण है
    • optional type chaining (जैसे a?.b?.c) संभव नहीं है। monadic types का support हो तो ज़्यादा सामान्य chaining संभव हो सकती थी, लेकिन अभी कमी है
    • lambda functions का support नहीं है। loop या catch block जैसी जगहों पर function blocks पहले से हैं, इसलिए अगर lambda support भी हो तो यह और flexible लगेगा
  • 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 बनाया जा सकता है

    • void type की बहुत पुरानी परंपरा है, जो ALGOL 68 तक जाती है। वहाँ VOID type को एक 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 भी बहुत सुंदर है

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

      Reason भी सुझाने लायक है। यह OCaml पर आधारित है, लेकिन इसकी syntax C-family जैसी है: reasonml.github.io