cat swift/map-compactmap-flatmap-filter-reduce.md

Lezione 18 55 min

Map, compactMap, flatMap, filter e reduce

Le trasformazioni sulle collection sono idiomatiche quando raccontano il flusso dei dati senza comprimere il dominio.

Swift offre molte funzioni per trasformare collection. Le più comuni sono:

  • map;
  • compactMap;
  • flatMap;
  • filter;
  • reduce.

Usate bene, rendono il codice più dichiarativo: descrivi cosa vuoi ottenere, non ogni passo meccanico. Usate male, creano catene dense e difficili da leggere.

map: trasformare ogni elemento

let nomi = prodotti.map { prodotto in
  prodotto.nome
}

map prende ogni elemento è lo trasforma in qualcos’altro.

Esempio nel carrello:

let subtotali = carrello.righe.map { riga in
  riga.subtotaleInCent
}

Il numero di elementi resta lo stesso. Cambia il tipo o il valore.

filter: tenere solo ciò che serve

let prodottiCostosi = prodotti.filter { prodotto in
  prodotto.prezzoInCent > 5000
}

filter non trasforma gli elementi. Decide quali restano.

let righeValide = carrello.righe.filter { riga in
  riga.quantita > 0
}

Se una condizione ha significato di dominio, valuta un metodo:

let righeValide = carrello.righe.filter(\.isValida)

compactMap: trasformare e scartare nil

compactMap è utile quando la trasformazione può fallire.

let codici = stringhe.compactMap { CodiceCoupon($0) }

Se CodiceCoupon($0) restituisce nil, quell’elemento viene scartato.

Esempio:

let couponValidi = inputCodici.compactMap { codice in
  CodiceCoupon(codice)
}

Attenzione: scartare valori non validi può essere corretto, ma a volte vuoi raccogliere errori. In quel caso compactMap nasconde informazioni.

flatMap: appiattire collection annidate

Se hai array dentro array:

let categoriePerOrdine: [[CategoriaProdotto]] = ordini.map { ordine in
  ordine.categorie
}

let tutteLeCategorie = categoriePerOrdine.flatMap { $0 }

Oppure direttamente:

let tutteLeCategorie = ordini.flatMap { ordine in
  ordine.categorie
}

flatMap è ottimo quando ogni elemento produce una collection e vuoi un’unica collection finale.

reduce: accumulare

let totale = carrello.righe.reduce(0) { totale, riga in
  totale + riga.subtotaleInCent
}

reduce prende un valore iniziale e accumula un risultato.

È potente, ma può diventare poco leggibile se l’accumulatore è complesso.

Se stai costruendo un dizionario o una struttura articolata, valuta un ciclo esplicito.

Catene leggibili

Una catena può essere molto chiara:

let totaleLibri = carrello.righe
  .filter { riga in riga.prodotto.categoria == .libri }
  .map { riga in riga.subtotaleInCent }
  .reduce(0, +)

Si legge come:

  1. prendi le righe;
  2. tieni quelle dei libri;
  3. estrai i subtotali;
  4. somma.

Questa è una buona catena.

Catene troppo dense

let x = a.flatMap { $0.b }.compactMap { C($0) }.filter { $0.v > 0 }.reduce(0) { $0 + $1.v }

Il problema non sono le funzioni. Il problema è che i nomi non comunicano.

Meglio:

let prodotti = ordini.flatMap { ordine in
  ordine.prodotti
}

let coupon = prodotti.compactMap { prodotto in
  Coupon(codice: prodotto.codicePromo)
}

let couponApplicabili = coupon.filter { coupon in
  coupon.puoEssereApplicato
}

let scontoTotale = couponApplicabili.reduce(0) { totale, coupon in
  totale + coupon.scontoInCent
}

Più righe, meno mistero.

Key path

Quando estrai una proprietà semplice, puoi usare key path:

let nomi = prodotti.map(\.nome)

È leggibile se la trasformazione è banale. Se devi aggiungere logica, torna alla closure esplicita.

Quando preferire un ciclo

Un ciclo e perfettamente idiomatico quando:

  • hai più condizioni con nomi intermedi;
  • devi gestire errori;
  • vuoi uscire presto;
  • l’accumulazione e complessa;
  • la catena diventerebbe difficile da debuggare.
var errori: [ErroreCoupon] = []

for regola in coupon.regole {
  do {
    try engine.valida(regola, nel: contesto)
  } catch let errore as ErroreCoupon {
    errori.append(errore)
  }
}

Questo è chiaro. Non serve forzarlo in reduce.

Checklist

Usa:

  • map per trasformare;
  • filter per selezionare;
  • compactMap per trasformazioni opzionali scartando nil;
  • flatMap per appiattire collection;
  • reduce per accumulare.

Fermati quando:

  • i nomi diventano $0, $1, $2 senza chiarezza;
  • la catena supera 3-4 passaggi non banali;
  • stai nascondendo errori;
  • un ciclo direbbe meglio l’intenzione.

Esercizio

Sul dominio carrello, scrivi:

  • elenco dei nomi prodotto;
  • totale delle righe;
  • totale solo per una categoria;
  • lista dei codici coupon validi usando compactMap;
  • lista piatta di categorie da più ordini usando flatMap;
  • raccolta errori con ciclo esplicito, spiegando perché non usi reduce.