ZIG - Iterazioni - for
Seconda iterazione, forse la più famosa, è introdotta dalla keyword for.
Si tratta anche in questo caso di una istruzione che ogni linguaggio
di programmazione he si rispetti ha nel suo arsenale.
In Zig for itera su collezioni o
su range, diciamo su tutto ciò che indicizzabile, ed in questo senso è un
po' diverso dall'omologa istruzione del C. Per esaminare questo
interessante elemento dovremo anticipare quanche concetto che vedremo
comunque nei paragrafi successivi.
Lo schema base è il seguente:
for (collezione) |elemento| {
istruzioni
}
Un primo semplice esempio è il seguente:
Esempio 10-1
const std = @import("std");
pub fn main() !void {
const arr = [_]i32{ 10, 20, 30 };
for (arr) |value| {
std.debug.print("{}\n", .{value});
}
}
const arr = [_]i32{ 10, 20, 30 }; definiamo
un array di dimensione fissa in cui con [_] diciamo al
compilatore di occuparsi lui di determinare la lunghezza dell'array stesso.
al nostro for viene passata la
collezione, in questo caso un array e successivamente l'elemento (value) in
cui riversare quanto recuperato nei vari passaggi fino ad esaurimento
dell'array stesso.
Il payload, ovvero l'elemento all'interno della coppia
| | è necessario definirlo anche se non lo utilizzate nel
qual caso si dovrà usare _ come nel seguente frammento di codice:
for (1..5) |_| {
std.debug.print("Ciao!\n", .{});
}
che stampa 4 volte (da 1 a 4 essendo il range non inclusivo dell'ultimo
elemento) il classico saluto e quanto è contenuto nel range 1..5 non
interessa se non ai fini del conteggio delle iterazioni.
Un discorso
interessante è quello che riguarda l'eventuale cambio di step durante
l'iterazioni su elementi sequenziali. Nel linguaggio C ad esempio si può
scrivere:
for (i = 0; i < n; i+=2)
ma in Zig non si può fare. E' una sorta di scelta "filosofica"?
Non lo so, negli intenti, credo, lo
step lo deve gestire il programmatore, non è un plus fornito da for (ma
da while invece si, il che a
prima vista mi suona un po' contraddittorio se devo essere
sincero). Quindi, supponiamo di avere un array che contenga tutti gli
elementi da 1 a 10 e di volere stampare solo i numeri pari. A parte
considerazioni sulla divisione per due che filtra gli elementi individuando
i pari e i dispari ma li fa passare tutti, come si potrebbe operare? In
pratica mi trovo ad avere questo array:const arr = [_]i32{
1,2,3,4,5,6,7,8,9,10 };
e voglio stampare solo i numeri
pari, che come vedremo quando parleremo degli array si trovano indicizzati
alle posizioni 1-3-5-7-9. Una prima soluzione è usare proprio while:
Esempio 10-2
const std = @import("std");
pub fn main() !void {
const arr = [_]i32 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var i: usize = 1;
while (i < arr.len) : (i += 2) {
std.debug.print("{}\n", .{arr[i]});
}
}
che fa esattamente quanto richiesto. Forse è il modo migliore, almeno io lo trovo più chiaro ed è legato ad una feature del linguaggio. Decisamente più elegante. Se vogliamo invece usare for possiamo fare come nel seguente codice:
Esempio 10-3
const std = @import("std");
pub fn main() !void {
const arr = [_]i32 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (0..arr.len / 2) |k| { // soluzione custom
const i = k * 2 + 1;
std.debug.print("{}\n", .{arr[i]});
}
}
Questa è una soluzione custom che va bene per questo caso ma evidentemente
non è idiomatica. Insomma, se c'è di mezzo lo step diverso da 1 io uso while.
Forse il punto è proprio questo a pensarci bene; dal momento che c'è già
while che permette questo tipo di operatività è
inutile darla anche a for, in momento che
sia una strada univoca. Magari è proprio così.
I punti di forza di for sono altri. Ad
esempio è molto semplice ricavare la sequenza degli indici di una
collezione. Prendiamo il solito array, struttura molto semplice,
per il seguente esempio:
Esempio 10-4
const std = @import("std");
pub fn main() !void {
const arr = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (arr, 0..) |value, i| {
std.debug.print("indice {} valore {}\n", .{ i, value });
}
}
che restituisce questo output:
| indice 0 valore 1 indice 1 valore 2 indice 2 valore 3 indice 3 valore 4 indice 4 valore 5 indice 5 valore 6 indice 6 valore 7 indice 7 valore 8 indice 8 valore 9 indice 9 valore 10 |
Nel codice dell'esempio 10.4 si noti che gli
elementi dell'array sono presi come al solito mentre gli indici sono
accolti da un range che, come vedremo quando ne parleremo, è infinito, non
avendo margine destro. In questo senso è adatto ad accogliere una
sequenza.
Se voleste usare due collezioni contemporaneamente,
non ci sono particolari problemi:
Esempio 10-5
const std = @import("std");
pub fn main() !void {
const arr1 = [_]i32{ 1, 2, 3, 4, 5 };
const arr2 = [_]i32{ 6, 7, 8, 9, 10 };
for (arr1, arr2) |a, b| {
std.debug.print("{} + {} = {}\n", .{ a, b, a + b });
}
}
da cui otteniamo:
| 1 + 6 = 7 2 + 7 = 9 3 + 8 = 11 4 + 9 = 13 5 + 10 = 15 |
Nel capitolo dedicato a while abbiamo citato due istruzioni atte a modificare il normale flusso delle operazioni di iterazione, ovvero break e continue. Esse continuano a valere anche nell'ambito del for ed hanno le medesime diciamo conseguenze. Anche in questo caso operiamo insieme ad if:
Esempio 10-6
const std = @import("std");
pub fn main() !void {
for (1..10) |x1| {
if (x1 == 3) continue;
if (x1 == 8) break;
std.debug.print("{} ",.{x1});
}
}
e qui abbiamo
| 1 2 4 5 6 7 |
saltando il 3 e interrompendo il ciclo quando si arriva al numero 8.
Nulla di diverso da quanto visto relativamente a while. Ma non finisce qui. Questa istruzione non solo ha il ruolo di permettere una interruzione ma, grazie all'uso di una etichetta, anche di uscire da più cicli innestati. Ecco l'esempio, basilare:
Esempio 10-7
const std = @import("std");
pub fn main() !void {
fuori: for (0..5) |i| {
for (0..5) |j| {
if (i == 1 and j == 2) {
break :fuori;
}
std.debug.print("{} {}\n", .{ i, j });
}
}
}
che restituisce il seguente output:
| 0 0 0 1 0 2 0 3 0 4 1 0 1 1 |
quindi, non solo il break permette l'uscita dal loop interno come normale ma, grazie all'etichetta fuori: il programa esce anche dal ciclo più esterno andando quindi alla fine del codice.
E non finisce qui. Forse ancora più interessante è il fatto che l'uso di break permette a for e anche a while, di essere usati come espressione, cosa che di loro natura non potrebbero fare. Il formato con cui si presenta questa nuova "natura" del for (e come detto è analogo per while) è la seguente:
const x = for (collection) |item| {
if
(condizione)
break valore;
} else valore_default;
ed ecco un esempio:
Esempio 10-8
const std = @import("std");
pub fn main() !void {
const x1 = for (0..10) |i| {
if (i == 4) break i * 2;
} else 0;
std.debug.print("x1 vale: {}\n", .{x1});
}
che ci restituirà x1 con il valore 8. Se provate un esempio con while otterrete lo stesso risultato. Questo procedimento ci permette quindi di usare entrambe queste iterazioni in modo "espressivo" attraverso un meccanismo molto semplice dal punto di vista della codifica.