Ho recentemente rilasciato un nuovo videocorso intitolato “Functional JavaScript“, 9 ore a spasso nei meandri della Programmazione Funzionale! Si parte dalle basi del paradigma, introducendo Immutabilità, Ricorsività, Manipolazione di Oggetti e Array, Funzioni Pure e composizione di funzioni: argomenti che a mio avviso ogni sviluppatore JavaScript dovrebbe non solo conoscere ma padroneggiare.

Nella parte finale del corso poi, ho deciso di includere una bella panoramica sugli ADT, arrivando a parlare di Funtori e Monadi, nella speranza che possano esservi utili in questo linguaggio o che vi spronino ad affrontare questi concetti anche in altri linguaggi di programmazione. A questo punto, se avete visto quella parte del corso, potreste pensare: in un framework come Angular, che di per sé offre moltissimi strumenti, questi concetti possono essermi utili? La risposta è assolutamente sì, anche se magari alcune strutture potrebbero essere meno utili rispetto ad altre. In particolare, però, due strutture possono esservi particolarmente utili: parlo di Option e Either (anche chiamate Maybe e Result), due Sum Type che vi permettono di modellare in modo preciso i vostri dati. In questo articolo vediamo un esempio semplice dell’utilizzo di Either, e visto che usiamo TypeScript ci affidiamo all’ottima libreria fp-ts di Giulio Canti. Iniziamo!

Lo scenario

Supponiamo di trovarci in una rotta di dettaglio, ad esempio /user/:id. Quello che vogliamo è il dettaglio dell’utente se c’è, altrimenti vogliamo mostrare un errore. Il primo approccio che può venirci in mente è quello di effettuare una chiamata all’OnInit e di salvare l’utente, o l’eventuale errore, in due proprietà. Per sfruttare al meglio il framework, utilizziamo due BehaviorSubject di RxJS per poterci avvalere di async pipe e non avere problemi di Change Detection:

@Component({
  selector: 'app-user-detail',
  template: `
    <h2 *ngIf="(user$ | async) as user">User: {{ user.name }}</h2>
    <h2 *ngIf="(error$ | async) as error">Error: {{ error }}</h2>
  `
})
export class UserDetailComponent {

  user$ = new BehaviorSubject<User | null>(null);
  error$ = new BehaviorSubject<string | null>(null);
}

Per effettuare la chiamata, ci iniettiamo la rotta corrente, recuperiamo l’ID e settiamo le proprietà:

import { EMPTY } from 'rxjs';

@Component({ ... })
export class UserDetailComponent implements OnInit {

  user$ = new BehaviorSubject<User | null>(null);
  error$ = new BehaviorSubject<string | null>(null);

  constructor(
    private route: ActivatedRoute,
    private http: HttpClient
  ) {}

  ngOnInit() {
    this.route.params.pipe(
      switchMap(({ id }) => this.http.get<User>(`.../users/${id}`).pipe(
        catchError(() => { this.error$.next('User not found'); return EMPTY; })
      ))
    ).subscribe(user => {
      this.user$.next(user);
      this.error$.next(null);
    })
  }
}

Tutto sembra funzionare, e funziona! Abbiamo però un problema e una scomodità: il problema è che in questo componente è perfettamente valido avere sia un utente sia un errore! Se stiamo attenti non capita, ma può capitare, e specialmente se il progetto è molto grande o coinvolge più persone, questo genere di cose può accadere di frequente: in sostanza, non abbiamo modellato correttamente i nostri dati. Vogliamo o l’utente o l’errore, mai entrambi.

La scomodità invece è quella di dover effettuare un subscribe: in Angular, grazie all’async pipe, dovrebbe essere sempre possibile fare a meno del subscribe manuale, ma in questo caso ne abbiamo bisogno perché dobbiamo gestire l’errore.

Come possiamo risolvere questi due problemi? Con la monade Either.

Introduciamo Either in fp-ts

Either è una struttura particolare: rappresenta il successo di un’operazione (il valore, nel nostro caso l’utente) oppure un errore (nel nostro caso, ci basta una stringa). In questo articolo non c’è il tempo di andare troppo nel dettaglio, ma funziona sostanzialmente così:

import { Either, left, right } from 'fp-ts/Either';

// Questa variabile è di tipo Either, può contenere una stringa di errore o un utente.
// Il tipo dell'errore, come di consueto in Programmazione Funzionale, è il primo argomento del generic.
let userOrError: Either<string, User>;

// Costruttore per il caso di successo
userOrError = right(myUser);

// Costruttore per il caso di errore
userOrError = left('User not found');

Abbiamo quindi una variabile che ha una bivalenza: contiene un valore di successo o un errore. Quindi il nostro valore viene imbottigliato. Ma non possiamo dare in pasto l’Either direttamente alla UI! Dobbiamo tirarci fuori il suo valore. Un’utility che possiamo usare è chiamata fold:

import { ..., fold } from 'fp-ts/Either';

const result = fold(
  err => err,
  user => user
)(userOrError)

La funzione fold accetta due callback: una per agire sull’errore, una sul valore di successo. Nel nostro caso, non ci interessa modificarli, possiamo tirarli fuori inalterati. Però, nel nostro caso, ci serviranno due variabili da dare in pasto alla UI: l’utente e l’errore. Quindi, possiamo pensare ad una cosa del genere:

// In caso sia un errore, questa variabile va a null
const user = fold(
  () => null,
  user => user
)(userOrError)

// In caso sia un successo, questa variabile va a null
const error = fold(
  err => err,
  () => null
)(userOrError)

Al posto delle funzioni che non fanno nulla, possiamo utilizzare l’utility identity (è semplicemente la funzione identità, messa a disposizione dalla libreria):

import { identity } from 'fp-ts/function';

// In caso sia un errore, questa variabile va a null
const user = fold(
  () => null,
  identity
)(userOrError)

// In caso sia un successo, questa variabile va a null
const error = fold(
  identity,
  () => null
)(userOrError)

Ora dobbiamo sfruttare queste conoscenze in accoppiata con RxJS per risolvere i nostri due problemi. In un colpo solo!

Soluzione

Come prima cosa, togliamo l’OnInit e usiamo una semplice proprietà della classe. Il risultato della chiamata dovrà essere mappato con un Right in caso di successo, altrimenti un Left: la nostra variabile quindi è un Observable<Either<string, User>>!

export class UserDetailComponent {

  private userOrError$: Observable<Either<string, User>> = this.route.params.pipe(
    switchMap(({ id }) => this.http.get<User>(`.../users/${id}`).pipe(
      map(right),
      catchError(() => of(left('User not found')))
    )),
    shareReplay(1)
  );

  user$ = new BehaviorSubject<User | null>(null);
  error$ = new BehaviorSubject<string | null>(null);

  constructor(
    private route: ActivatedRoute,
    private http: HttpClient
  ) {}
}

Dato che il nostro Observable verrà dato in pasto alla UI con la async pipe, non dimenticare di usare shareReplay(1) per evitare di ripetere la chiamata http ad ogni sottoscrizione!

A questo punto, le nostre due proprietà diventano semplicemente due stati derivati del nostro Observable:

export class UserDetailComponent {

  ...

  user$: Observable<User | null> = this.userOrError$.pipe(
    map(fold(() => null, identity)) // equivalente: map(getOrElse(() => null))
  );

  error$: Observable<string | null> = this.userOrError$.pipe(
    map(fold(identity, () => null))
  );

  ...
}

E abbiamo finito! Notate una distinzione: la proprietà originale è private, possiamo spostarla in un servizio o utilizzarla in uno stato di Redux a nostro piacimento, ed è la sorgente di verità del nostro dato. Invece, queste due proprietà appena scritte appartengono alla UI e pertanto sono nullable: volendo, se utilizziamo Redux, possiamo usare dei selettori in modo che i nostri componenti non debbano nemmeno accorgersi che stiamo usando la libreria fp-ts.

L’esempio completo funzionante è disponibile a questo link!

Conclusioni

Cosa abbiamo ottenuto quindi, in questo semplice esercizio?

  • Gestione dell’errore con Either
  • Completamente reattivo!
  • Nessun subscribe()
  • Nessun BehaviorSubject, nessun next() imperativo
  • Niente lifecycle hooks
  • Volete ottimizzare le performance con ChangeDetectionStrategy.OnPush? Potete farlo!

A cosa servono le monadi quindi? Beh, in questo esempio, per modellare dati e comporre funzioni. Sono utilissime per gestire errori ed eccezioni, valori nulli, side-effect sincroni e asincroni.

Se siete curiosi e volete saperne di più, o se non vi è chiaro quello che abbiamo fatto in questo articolo, il corso Functional JavaScript è fatto apposta per voi! 😎 Non tratta fp-ts nello specifico ma affronta questi argomenti in modo generico, e queste strutture vengono ricostruite passo passo in Vanilla JS al posto di TypeScript. Non escludo di dedicare un capitolo del corso a questa bellissima libreria in futuro!

Ti è piaciuto l’articolo? Condividilo!

Rilasciare risorse gratuite non è facile: un sacco di tempo vola fra la ricerca dell’idea, la stesura, la preparazione di codice funzionante e la revisione. Se ti piace ciò che facciamo, condividi l’articolo: nel peggiore dei casi ti daranno del secchione!

Ti interesserà anche…

Angular

Gestione degli errori

In Angular abbiamo diversi modi per gestire gli errori: ognuno ha i suoi pro e i suoi contro, vediamoli assieme. HttpErrorResponse Il caso più comune di errore da dover gestire…

Iscriviti alla nostra newsletter!

Rimani aggiornato per quando rilasceremo nuovi articoli, nuovi corsinuove promozioni e nuove risorse gratuite. Ti promettiamo che useremo la tua email con prudenza, e non la condivideremo con nessuno senza il tuo consenso!

Niente spam. Leggi la nostra privacy policy qui.

Menu