ZIG - Variabili e costanti


Eccoci arrivati ad un argomento fondamentale per ogni linguaggio che si rispetti. La definizione della variabili, o meglio degli elementi deputati ad indentificare i dati nell'ambito dei nostri programmi segue norme molto precise, come nella filosofia di Zig.
La prima cosa che dobbiamo esaminare sono i nomi o meglio appunto gli identificatori che possiamo usare. Poche semplici regole:

Non vi sono, a quanto ne so, limiti alla lunghezza degli identificatori, ovviamente meglio non esagerare per chiarezza nella lettura del codice.

Per costruire gli identificatori:
1) possono iniziare con lettere o underscore (-)
2) seguono, lettere, numeri o ancora underscore.

Non possono essere usate, ovviamente, le keyword del linguaggio ma è possibile bypassare queste limitazioni, se proprio necessario ad esempio in caso di interoperabilità con altri linguaggi, con la sequenza @".." ad esempio, se fossimo costretti a definire un elemento costante usando l'identificatore "var", che è keyword del linguaggio, come vedremo tra breve, potremmo scrivere:

const @"var":i32 = 0;

che è istruzione ammessa.

Si usano le seguenti convenzioni:
1) snake_case per variabili, funzioni che non restituiscono valori, namespace.
2) PascalCase per i tipi o le strutture, array ecc..
3) camelCase per funzioni che restituiscono un valore
tali convenzioni, peraltro consigliate, non sono stringenti nel senso che il compilatore non vi obbliga ad usarle.

Un caso particolare è costituito dall'identificatore _ (ovvero il solo carattere underscore). Bene, il suo significato non è quello di determinare un vero e proprio elemento, ma significa in breve "ignora questo valore". Vedremo i casi in cui si può applicare.

Bene, ora che ci siamo tolti questo problema, affrontiamo una questione più interessante ovvero le keyword del linguaggio disponibili per definire i nostri elementi: sostanzialmente sono due:

La differenza non è solo di comportamento ma anche a livello di dichiarazione, ovvero il classico binding tra un valore, che di solito sta a destra e l'identificatore, che sta a sinistra. Vediamo in pratica il modo corretto per dichiarare variabili e costanti senza avere problemi con il compilatore:

Vedremo poi un'altra possibilità interessante ma per ora concentriamoci su questi casi e vediamo qualche esempio:

var x1: i32 = 3;
const x2 = 3;
const x3: i32 = 3;

Per la creazione di esempi e ancor di più per i vostri programmi ricordate bene alcune regole fondamentali:
1) ogni cosa che viene dichiarata deve essere usata. Il compilatore vi bacchetta e non compila se trova qualcosa che sia stato dichiarato e non utilizzato che sia var o const.
2) ogni variabile (dichiarato quindi tramite var) deve essere modificata nel corso del programma. Ovvero se dichiarate un elemento con var questo deve essere non solo utilizzato ma anche soggetto ad almeno una operazione di modifica. Altrimenti va dichiarato const.
Tenete presente che Zig lavora al meglio proprio con i valori costanti in quanto prevede ottimizzazioni specifiche per questi. Vediamo dal punto di vista del codice cosa comportino i due punti precedenti.

Esempio 3.1

const std = @import("std");
pub fn main() !void {
  var x = 0;
  std.debug.print("a {}\n", .{@TypeOf(x)});
}

Questo codice (che presenta la funzione built-in TypeOf(elemento) che ci permette di ricavare il tipo di una variabile o di una costante) non compila e presenta 2 errori che in confermano quanto detto in precedenza:

z112.zig:3:9: error: local variable is never mutated
var x = 0;
^
z112.zig:3:9: note: consider using 'const'
z112.zig:3:9: error: variable of type 'comptime_int' must be const or comptime
var x = 0;
^
z112.zig:3:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type

I due errori evidenziati in blu confermano che è un errore non modificare un elemento dichiarato var e che bisognerebbe definirlo tramite const. La riga in verde ci dice che un elemento modificabile deve avere un tipo esplicito indicato. La riga in nero la discuteremo più avanti nel paragrafo. Se non ci fosse stata l'istruzione di stampa il primo errore sarebbe stato sostituito da una segnalazione di variabile non usata. Quindi come può funzionare il codice precedente? Ad esempio come segue, in formato minimale:

const std = @import("std");
pub fn main() !void {
  var x: i32 = 0;
  x = x + 1;
}

Tutto funziona in quanto la variabile x è stata modificata nell'ambito di una operazione aritmetica e ad essa era stato attribuito un tipo preciso.
Per le costanti invece il discorso è simile ma c'è qualcosa di particolare da osservare.

Esempio 3.2

const std = @import("std");
pub fn main() !void {    
  const x: i32 = 0;    
  std.debug.print("tipo di x {}\n", .{@TypeOf(x)});    
  const y = 0;    
  std.debug.print("tipo di y {}\n", .{@TypeOf(y)});
} 

Questo programma prevede la presenza di due costanti definite tramite esplicitazione del tipo alla riga 3 e tramite inferenza alla 5. Tuttavia l'output ci fornisce una piccola sorpresa:

tipo di x i32
tipo di y comptime_int

La prima riga di output è ovvia e normale e deriva dalla attribuzione esplicita di un tipo alla costante che avviene alla riga 3. La seconda riga di output ci offre un risultato inatteso, ovvero il tipo di y è comptime_int. Qui si entra in un campo piuttosto complicato ma il concetto di comptime è centrale in Zig. Lo amplieremo più avanti per adesso dobbiamo capire cos'è comptime_int. Si tratta di un tipo numerico anomalo, che esiste solo a compile time, come il suo stesso nome fa intuire. Si può definire come il tipo numerico intero assoluto, senza limiti, senza segno, senza overflow, senza una vera natura finchè non serve. Potremmo anche scrivere:

const x = 99999999999999999999999999;

che il compilatore lo accetterebbe perchè non è un numero intero runtime. Ovviamente non ha neanche segno, è puro numero manipolabile a compile time e come tale non può essere assegnato, come abbiamo visto, ad una variabile che invece necessita di limiti precisi. Quindi, cosa ce ne facciamo? Come abbiamo detto comptime_int non ha una vera natura finchè non serve. Quando l'elemento così definito viene usato allora anche la sua natura viene decisa. E la "trasformazione" può avvenire al volo con diversi tipi:
esempio 3.3

const std = @import("std");
pub fn main() !void {    
  const y = 10;    
  var x: i8 = 20;    
  x = x + y;    
  std.debug.print("x: {}\n", .{x});    
  std.debug.print("y: {}\n", .{@TypeOf(y)});    
  var z: i32 = 30;    
  z = z + y;    
  std.debug.print("z: {}\n", .{z});    
  std.debug.print("y: {}\n", .{@TypeOf(y)}); 
}

L'output è il seguente:

x: 30
y: comptime_int
z: 40
y: comptime_int

Quindi la costante di tipo comptime_int si lega tranquillamente al volo prima con un i8 poi con un i32. La sua natura "universale" permette tutto questo. Ad ogni modo, la discussione su sul concetto comptime non finisce qui e ne parleremo ancora molto.
Zig, come avrete intuito, impone di inizializzare ogni elemento che si usa ma non permette inizializzazioni multiple. Quindi istruzioni tipo:

const x, y, z = 1, 2, 3;

non sono permesse. La chiarezza innanzitutto, ogni elemento deve essere dichiarato esplicitamente e in forma indipendente dagli altri e quindi l'attribuzione dei valori iniziali è fatta comunque separatamente. Se proprio volete inizializzare gruppi di variabili potrebbe essere necessario utilizzare le strutture, come vedremo, sempre che non cambi nulla da qui alla versione 1.0 ma direi che su questo aspetto la cosa dovrebbe essere stabile.
Non è finita qui, vediamo la seguente definizione di una variabile:

var x1: i32 = undefined;

Questa è perfettamente valida per il compilatore Zig. Cosa significa quel undefined?  Qualcuno lo definisce "spazzatura". Altri, più elegantemente e in maniera più propria "segnaposto semantico". Mi piace anche la parola "promessa". In pratica diciamo al compilatore di predisporre, bloccare il nome di quell'elemento che sarà successivamente inizializzato in maniera congrua per essere poi utilizzato. Sia chiaro che non equivale a "null" o "nil" di altri linguaggi, tantomeno è un valore di default. Ovviamente, undefined si può usare solo per le variabili vere e proprie, una costante deve avere un valore noto già in compilazione.
Esempio 3.4

const std = @import("std");
pub fn main() !void {
  const y1 = 12;
  var x1: i8 = undefined;
  x1 = 5;
  x1 = x1 + y1;
  std.debug.print(" x1: {}\n", .{x1});
}
Questo semplice programma stampa il valore 17, come prevedibile. Quello che deve essere chiaro, va ribadito, è che prima dell'utilizzo deve esservi una inizializzazione reale, con un valore adeguato al tipo eventualmente specificato. Da quanto si è visto, si evince che la dichiarazione, possibile analoga in altri linguaggi, tipo:

var x: i32;

non è permessa anche se successivamente si provvedesse ad una inizializzazione la quale, anche questo è normale ma va sottlineato, può avvenire anche attraverso espressioni un po' più complesse:

var x1: i8 = 4 * (2 + 5);
const x2 = 4 + (4 / 2);
x1 = x1 + x2;

o anche come valori di ritorno di una funzione ma lo vedremo più avanti. In questo ultimo caso però una costante può essere inizializzata solo tramite un funzione nota a compile-time o in un contesto che non richieda valutazioni a compile time.