7 points par GN⁺ 2025-08-22 | 1 commentaires | Partager sur WhatsApp
  • Zig repose sur une syntaxe à accolades proche de Rust, mais l’améliore avec une sémantique de langage plus simple et des choix syntaxiques plus élégants
  • Les littéraux entiers commencent tous avec le type comptime_int et sont convertis explicitement lors de l’affectation, tandis que les littéraux de chaîne utilisent une notation de chaîne brute concise basée sur \\
  • Les littéraux d’enregistrement sous la forme .x = 1 rendent les écritures de champs faciles à rechercher, et tous les types sont exprimés de manière cohérente en notation préfixée
  • and et or sont utilisés comme mots-clés de contrôle de flux, et les constructions if et loop peuvent omettre les accolades de manière optionnelle, le formateur garantissant la sûreté
  • Sans espace de noms, tout est traité comme une expression, ce qui unifie la syntaxe des types, des valeurs et des motifs, tout en permettant d’utiliser de façon concise les génériques, les littéraux d’enregistrement et les fonctions intégrées (@import, @as, etc.)

Vue d’ensemble

  • Zig a une apparence proche de Rust, mais adopte une structure de langage plus simple
  • La conception de sa syntaxe met l’accent sur la compatibilité avec grep, la cohérence syntaxique et la réduction du bruit visuel inutile

Littéraux entiers

const an_integer = 92;  
assert(@TypeOf(an_integer) == comptime_int);  
  
const x: i32 = 92;  
const y = @as(i32, 92);  
  • Tous les littéraux entiers ont le type comptime_int
  • Lors de l’affectation à une variable, il faut soit préciser explicitement le type, soit convertir avec @as
  • La forme var x = 92; ne fonctionne pas et nécessite un type explicite

Littéraux de chaîne

const raw =  
    \\Roses are red  
    \\  Violets are blue,  
    \\Sugar is sweet  
    \\  And so are you.  
    \\  
;  
  • Chaque ligne étant un jeton distinct, il n’y a pas de problème d’indentation
  • Il n’est pas nécessaire d’échapper \\ lui-même

Littéraux d’enregistrement

const p: Point = .{  
    .x = 1,  
    .y = 2,  
};  
  • Le format .x = 1 facilite la distinction entre lecture et écriture
  • La notation .{} se distingue d’un bloc tout en étant automatiquement convertie vers le type de résultat

Notation des types

u32        // entier  
[3]u32     // tableau de longueur 3  
?[3]u32    // tableau nullable  
*const ?[3]u32 // pointeur constant  
  • Tous les types utilisent une notation préfixée
  • Le déréférencement utilise une notation suffixée (ptr.*)

Identifiants

const @"a name with space" = 42;  
  • Permet d’éviter les collisions avec des mots-clés ou de définir des noms spéciaux

Déclaration de fonctions

pub fn main() void {}  
fn add(x: i32, y: i32) i32 {  
    return x + y;  
}  
  • Le mot-clé fn est collé au nom de la fonction, ce qui facilite la recherche
  • La notation du type de retour n’utilise pas ->

Déclaration de variables

const mid = lo + @divFloor(hi - lo, 2);  
var count: u32 = 0;  
  • Utilise const et var
  • La notation des types suit l’ordre nom: type

Contrôle de flux : and/or

while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {  
    count -= 1;  
}  
  • and et or sont des mots-clés de contrôle de flux
  • Les opérations bit à bit utilisent & et |

Instruction if

.direction = if (prng.boolean()) .ascending else .descending;  
  • Les parenthèses sont obligatoires, les accolades optionnelles
  • zig fmt garantit un formatage sûr

Boucles

for (0..10) |i| {  
    print("{d}\n", .{i});  
} else @panic("loop safety counter exceeded");  
  • for comme while prennent en charge une clause else
  • L’itérateur et le nom de l’élément sont placés de manière intuitive

Espaces de noms et résolution des noms

const std = @import("std");  
const ArrayList = std.ArrayList;  
  • Le masquage de variables est interdit
  • Il n’y a ni espaces de noms ni imports globaux

Tout est expression

const E = enum { a, b };  
const e: if (true) E else void = .a;  
  • Unifie la syntaxe des types, des valeurs et des motifs
  • Il est possible de placer une expression conditionnelle à l’emplacement d’un type

Génériques

fn ArrayListType(comptime T: type) type {  
    return struct {  
        fn init() void {}  
    };  
}  
  
var xs: ArrayListType(u32) = .init();  
  • Les génériques sont exprimés avec une syntaxe d’appel de fonction (Type(T))
  • Les arguments de type sont toujours explicites

Fonctions intégrées

const foo = @import("./foo.zig");  
const num = @as(i32, 92);  
  • Le préfixe @ appelle les fonctionnalités fournies par le compilateur
  • @import rend clairement visible le chemin du fichier
  • Les arguments doivent obligatoirement être des littéraux de chaîne

Conclusion

  • La syntaxe de Zig montre comment un ensemble de petits choix peut produire un langage agréable à lire
  • Réduire le nombre de fonctionnalités réduit aussi la quantité de syntaxe nécessaire, et diminue les risques de conflits syntaxiques
  • Le langage emprunte les bonnes idées des langages existants, tout en introduisant sans hésiter de nouvelles formes syntaxiques lorsque c’est nécessaire

1 commentaires

 
GN⁺ 2025-08-22
Discussion sur Hacker News
  • Cet article traite en profondeur des nombreux compromis de la conception de syntaxe, et j’ai été vraiment impressionné par le minimalisme, la cohérence et l’attention presque impitoyable portée à la lisibilité dans la syntaxe de Zig. Ce que j’aime, ce n’est pas une beauté abstraite, mais une forme de « brutalisme » sans surprise pour des usages industriels. Ce genre de syntaxe bien équilibrée est vraiment rare, et je pense que Zig a très bien réussi cela

    • C’est dommage que l’article ne mentionne pas la gestion des erreurs. L’approche try/catch de Zig est excellente, et c’est probablement ma façon préférée de gérer les erreurs parmi plusieurs langages. J’aurais aimé que ce point soit aussi présenté

    • Le vrai attrait de Zig ne réside pas dans une « belle lisibilité en surface », mais dans une beauté cohérente obtenue par l’abstraction. Comme dans l’analogie entre S-expressions et M-expressions, une bonne approche pour le cas général est souvent meilleure à long terme qu’une conception spéciale pour une multitude de cas particuliers. Si l’on ajoute des cas d’exception à la C++, on finit seulement par devoir mémoriser toutes les règles. En conception de langage, si l’on poursuit la simplicité et la cohérence de façon trop absolue, on peut aussi tomber dans un « Turing tarpit » où la complexité est finalement reportée sur l’utilisateur ; il est donc important d’avoir une approche où les cas particuliers se résolvent naturellement à partir de règles générales. On en voit un bon exemple dans le comic XKCD New Pet

    • Si quelqu’un a un exemple qui l’a particulièrement marqué, je serais curieux qu’il le partage

  • À propos du fait que Zig, comme Rust, utilise la forme nom:type pour l’annotation de type, je préfère malgré tout l’approche traditionnelle où le type vient d’abord. Quand je reviens vérifier une déclaration de variable, la première chose que je veux savoir est son type, et ne pas le trouver rapidement est gênant. En particulier, Rust a beaucoup d’éléments inutilement répétés comme let mut, ce qui le rend au contraire plus lourd, et j’aime bien aussi l’approche de C et C++ où le type vient en premier. En pratique, je pense qu’il est idéal de n’utiliser l’inférence de type que là où elle est vraiment nécessaire

    • Le mot-clé let a quand même son utilité, car il rend explicite le fait qu’il s’agit d’une déclaration. Sans cela, on peut se retrouver avec les problèmes d’analyse syntaxique ambigus de C++

    • Moi aussi, j’essaie toujours de vérifier le type d’une variable en premier, donc je préfère la syntaxe où le type vient avant. Du point de vue du parseur, commencer par le nom est plus pratique, et je comprends que TypeScript ait adopté cette structure pour des raisons de compatibilité avec JavaScript. Au final, je pense que l’essentiel est d’avoir une bibliothèque standard simple à utiliser. Comme dans les exemples où le système de types est abusé à l’excès, il est souvent plus important de transmettre clairement l’intention que d’exprimer de force chaque état dans les types

    • Je remonte dans le code pour vérifier le type d’une variable, mais quand le type est placé avant, il devient au contraire plus difficile de retrouver la déclaration de variable que je cherche. Le nom du type arrive tout au début, sa longueur varie, et cela m’oblige à balayer le regard de gauche à droite de manière répétée, ce qui me paraît inefficace

    • Dans la plupart des cas, l’éditeur affiche immédiatement les informations de type au survol, donc l’emplacement du type dans le code n’est peut-être pas si important. Si Rust est verbeux, c’est surtout pour éviter les ambiguïtés de parsing. Avec C et C++, où le type vient d’abord, il est difficile de retrouver facilement avec grep les variables déclarées sous un certain nom, et le style qui place le type de retour devant a été introduit à cause des templates, mais dans certains cas il rend aussi le code plus facile à lire et à retrouver

    • Personnellement, je préfère le style d’annotation de type à la Pascal. Même quand on veut faire de l’inférence de type, on n’a pas besoin d’un mécanisme détourné comme auto, et du point de vue du parsing c’est moins ambigu. Dans MyClass x, il est difficile de savoir immédiatement si MyClass est un type ou un nom de variable, donc cette approche réduit ce genre d’ambiguïté

  • Concernant la syntaxe des raw/multiline strings de Zig, le fait de devoir écrire plusieurs \\ me paraît beaucoup trop déroutant et extrême

    • Si vous avez déjà essayé de formater des chaînes multilignes en Python, C++, Rust, etc., vous comprendrez cet inconfort. Il y a toujours le problème de l’indentation incluse dans le contenu de la chaîne, et les modes de suppression d’indentation comme en YAML ont plutôt tendance à ajouter de la confusion. L’approche de Zig est très claire sur la question de l’indentation

    • Au début, j’ai trouvé cette syntaxe très pénible, mais à mesure qu’on utilise Zig, on finit par s’y habituer et même à en voir les avantages. Zig a ce côté où certaines choses peuvent déplaire au premier abord, mais dont on comprend l’intérêt à l’usage

    • En réalité, ce n’est pas une syntaxe folle, c’est un problème fou à résoudre : celui d’insérer proprement une chaîne multiligne à l’intérieur d’une autre chaîne multiligne. Dans Zig, c’est agréable de ne pas avoir besoin d’escape supplémentaire ni de se soucier de l’indentation

    • trimIndent de Kotlin, les text blocks de Go ou Java, et surtout les raw strings avec backticks de Go me paraissent plus fluides. Dans Zig, à cause des \\, j’utilise plutôt @embedFile comme solution de contournement

    • Visuellement, je n’aime pas spécialement les \\, mais je pense que c’est une manière propre de résoudre les problèmes de littéraux multilignes et d’indentation. Je ne connais pas vraiment d’autre langage qui règle ce problème sans passer par une fonction

  • La syntaxe de Zig me paraît désordonnée. Les formes qui commencent par @, comme @TypeOf, ou la syntaxe d’initialisation comme .{.x} me semblent étranges. C’est peut-être parce que je ne maîtrise pas encore bien Zig, mais dans l’ensemble j’ai l’impression que le code est difficile à lire

    • Je préfère la syntaxe d’Odin, qui est bien plus minimaliste et mieux polie. Zig donne une impression un peu brouillonne

    • Le . sert dans Zig de placeholder pour un type inféré. On peut par exemple initialiser un objet comme ceci

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

      ou, si l’on veut expliciter l’inférence de type,

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

      On peut aussi omettre le type dans les arguments de fonction, ce qui rend le tout plus concis. En Rust, il faut écrire le type explicitement dans ce genre de situation

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

      Dans l’initialisation de structures imbriquées aussi, l’inférence de Zig est bien plus utile. En Rust, devoir écrire les types explicitement partout peut vite rendre le code visuellement chargé. Cela dit, je trouverais plus pratique de supprimer le point initial, mais il semble être conservé pour simplifier l’implémentation du parseur. Les notations x: 123 et .x = 123 sont empruntées respectivement à JS et à C99. Personnellement, comme j’utilise souvent les deux, cela ne me paraît pas étrange

  • Je préfère largement la manière dont C# 11 gère les raw string literals. L’indentation des autres lignes est automatiquement alignée à partir de l’indentation de la première ligne. On peut aussi utiliser les accolades comme caractères littéraux. Quand plusieurs $ apparaissent, les accolades sont complètement traitées comme des valeurs littérales

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (En tant qu’auteur de la fonctionnalité raw string literal de C#) en réalité, c’est l’indentation de la ligne finale """ qui sert de référence, et la première ligne peut elle aussi être indentée. Ça me fait plaisir que cette fonctionnalité vous plaise, et je pense sincèrement que c’est une bonne fonctionnalité
  • La syntaxe de Zig est bonne, mais comme Go montre qu’on peut écrire quelque chose de très propre même sans point-virgule ni :, je n’irais pas jusqu’à la qualifier de « lovely ». En comparaison, c’est clairement bien meilleur que Rust, mais Go est lui aussi excellent

    • Au contraire, une syntaxe trop minimaliste comme celle de Go peut parfois être plus difficile à interpréter à la lecture. On passe plus de temps à lire du code qu’à en écrire, donc une concision excessive peut au contraire favoriser les erreurs et compliquer le débogage. CoffeeScript ou J sont des exemples typiques de syntaxes trop condensées

    • Je ne pense pas que retirer des éléments syntaxiques rende automatiquement une syntaxe meilleure. Si c’était le cas, tout le monde écrirait en Lisp, et on écrirait aussi comme en scriptio continua (ancienne écriture sans espaces). Voir l’article Wikipédia sur la scriptio continua

  • Zig me satisfait globalement, mais les points suivants sont regrettables

    • il est difficile de spécifier la valeur de retour d’un bloc. Il serait agréable, comme en Rust, que la dernière expression soit automatiquement reconnue comme valeur de retour, mais dans Zig il faut utiliser un label ou autre, ce qui est fastidieux
    • le chaînage des types optionnels (par ex. a?.b?.c) n’est pas possible. Avec un support des types monadiques, un chaînage plus général serait possible, mais pour l’instant cela reste insuffisant
    • il n’y a pas de support des fonctions lambda. On peut déjà utiliser des blocs de fonction dans des boucles ou des blocs catch, donc ajouter les lambdas rendrait le langage plus flexible
  • Concernant l’usage de void comme nom de type, en théorie des types, void ne correspond pas au rôle de unit, mais à un type non habité, c’est-à-dire sans valeur. Traditionnellement, () ou unit désigne un type avec un seul membre. void serait plutôt le type de retour d’une fonction comme abort

    • En C et C++, void est utilisé de manière tout à fait acceptable, donc beaucoup de programmeurs système y sont habitués. Je pense que les débats terminologiques de théorie des types n’ont pas beaucoup d’intérêt en pratique. Beaucoup de gens qui viennent à Zig ont un bagage C/C++, donc void est très bien

    • abort relève plutôt d’un type d’« inaccessibilité » comme le type ! de Rust. void est plus proche de unit ou (), et correspond davantage à un type sans valeur exploitable. Petit truc amusant : en TypeScript, si on utilise void dans une contrainte générique, on peut rendre ce paramètre optionnel

    • Le type void a une très longue histoire, qui remonte jusqu’à ALGOL 68. Là-bas, le type VOID est défini comme un type avec un seul membre (EMPTY)

  • Je suis surpris d’apprendre que « Zig n’a pas de lambdas ». En C++, j’utilise des lambdas presque partout, donc je me demande comment on définit par exemple un comparateur pour trier un tableau

    • En général, comme il faut déclarer la fonction séparément, je trouve ce point peu pratique en Zig

    • On peut référencer en ligne une structure anonyme et les fonctions qu’elle contient. Zig n’a pas le mécanisme de capture souvent utilisé avec les lambdas, mais on peut le remplacer en passant un paramètre de contexte, généralement une structure

    • Fondamentalement, comme en C, on déclare une fonction séparée puis on passe son pointeur à la fonction de tri

  • On dit souvent que « la syntaxe n’a pas d’importance », mais en pratique cela revient souvent à dire « la syntaxe n’a pas d’importance, donc utilisons celle que je préfère ». Moi aussi, je suis à l’aise avec les syntaxes dérivées de C comme Rust/Zig/Go, et les styles comme Haskell/OCaml où l’appel de fonction est séparé par des espaces me restent encore peu familiers ; je pense que cela freine leur adoption. Comme le succès de Rust l’a montré, d’autres langages pourraient s’inspirer de la manière dont il a incorporé les « épinards » de la programmation fonctionnelle dans le « brownie » d’un langage système

    • Je ne suis pas d’accord avec l’idée que la syntaxe n’a pas d’importance. En fin de compte, la syntaxe est l’interface principale à travers laquelle l’utilisateur interagit avec le langage. Chaque fois que je lis un langage, ses éléments syntaxiques ressortent inconsciemment de façon très marquée

    • Si vous voulez un langage fonctionnel avec une syntaxe de style C, je recommande Gleam : gleam.run Le code est aussi très joli

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

      Reason vaut aussi le détour. C’est basé sur OCaml, mais avec une syntaxe de style C : reasonml.github.io