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.