Track
/
JavaScript
JavaScript
/
Esercizi
/
Servizio di traduzione
Servizio di traduzione

Servizio di traduzione

Esercizio di apprendimento

Introduzione

L'oggetto Promise rappresenta il completamento (o il fallimento) futuro di un'operazione asincrona e il valore che ne risulta.

Note

Questo è un argomento difficile per molte persone, soprattutto se sai programmare in un linguaggio completamente sincrono. Se ti senti sopraffatto, o vorresti saperne di più su concorrenza e parallelismo, guarda (su go.dev) o guarda direttamente su vimeo e leggi le slide del brillante intervento «Concurrency is not parallelism».

Ciclo di vita di una promise

Una Promise ha tre stati:

  1. pending
  2. fulfilled
  3. rejected

Quando viene creata, una promise è nello stato pending. A un certo punto nel futuro potrà resolve o reject. Una volta che una promise raggiunge lo stato resolved o rejected, non potrà mai più diventare resolved o rejected, né il suo stato può cambiare.

In altre parole:

  1. Nello stato pending, una promise:
    • può passare allo stato fulfilled o rejected.
  2. Nello stato fulfilled, una promise:
    • non deve passare a nessun altro stato.
    • deve avere un valore, che non deve cambiare.
  3. Nello stato rejected, una promise:
    • non deve passare a nessun altro stato.
    • deve avere un motivo, che non deve cambiare.

Risolvere una promise

Una promise può essere risolta in vari modi:

// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
  resolve(value);
});

// Chaining a promise leads to a resolved promise
somePromise.then(() => {
  // ...
  return value;
});

Negli esempi qui sopra value può essere qualsiasi cosa, incluso un errore, undefined, null o un'altra promise. Di solito vorrai risolvere con un valore che non sia un errore.

Rifiutare una promise

Una promise può essere rifiutata in vari modi:

// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
  reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
  // ...
  throw reason
})

Negli esempi qui sopra reason può essere qualsiasi cosa, incluso un errore, undefined o null. Di solito vorrai rifiutare con un errore.

Concatenare una promise

A una promise si può collegare un'azione futura, eseguita quando si risolve o si rifiuta.

then

Ogni promise è «thenable». Questo significa che è disponibile una funzione then che verrà eseguita una volta che la promise originale è risolta. Dato promise.then(onResolved), la callback onResolved riceve il valore con cui è stata risolta la promise originale. Questa restituirà sempre una nuova promise «concatenata».

Restituire un value da then risolve la promise «concatenata». Lanciare un reason in then rifiuta la promise «concatenata».

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

const promise2 = promise1.then(function (value) {
  console.log(value);
  // expected output: "Success!"

  return true;
});

Questo registrerà "Success!" dopo circa 1000 ms. Lo stato e il valore di promise1 saranno resolved e "Success!". Lo stato e il valore di promise2 saranno resolved e true.

È disponibile un secondo argomento che viene eseguito quando la promise originale viene rifiutata. Dato promise.then(onResolved, onRejected), la callback onResolved riceve il valore con cui è stata risolta la promise originale, oppure la callback onRejected riceve il motivo per cui la promise è stata rifiutata.

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return true;
}

function shout(reason) {
  console.error(reason.toUpperCase());
  return false;
}

const promise2 = promise1.then(log, shout);
  • In circa 1/2 dei casi, registrerà "Success!" dopo circa 1000 ms.
    • Lo stato e il valore di promise1 saranno resolved e "Success!".
    • Lo stato e il valore di promise2 saranno resolved e true.
  • In circa 1/2 dei casi, registrerà immediatamente "NOPE!".
    • Lo stato e il valore di promise1 saranno rejected e Nope!.
    • Lo stato e il valore di promise2 saranno resolved e false.

È importante capire che, a causa delle regole del ciclo di vita, quando la promise esegue reject, la resolve che arriva circa 1000 ms dopo viene ignorata silenziosamente: lo stato interno non può cambiare una volta che è diventato rejected o resolved. È importante capire che restituire un valore da una promise la risolve, e lanciare un valore la rifiuta. Quando promise1 si risolve e c'è un onResolved concatenato: then(onResolved), quella continuazione è una nuova promise che può risolversi o rifiutarsi. Quando promise1 viene rifiutata ma c'è un onRejected concatenato: then(, onRejected), quella continuazione è una nuova promise che può risolversi o rifiutarsi.

catch

A volte vuoi catturare gli errori e continuare solo quando la promise originale esegue reject. Dato promise.catch(onCatch), la callback onCatch riceve il motivo per cui la promise originale è stata rifiutata. Questa restituirà sempre una nuova promise «concatenata».

Restituire un value da catch risolve la promise «concatenata». Lanciare un reason in catch rifiuta la promise «concatenata».

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return 'done';
}

function recover(reason) {
  console.error(reason.toUpperCase());
  return 42;
}

const promise2 = promise1.catch(recover).then(log);

In circa 1/2 dei casi, registrerà "Success!" dopo circa 1000 ms. Nell'altro 1/2 dei casi, registrerà immediatamente 42.

  • Se promise1 si risolve, catch viene saltata, si arriva a then e viene registrato il valore.
    • Lo stato e il valore di promise1 saranno resolved e "Success!".
    • Lo stato e il valore di promise2 saranno resolved e "done";
  • Se promise1 viene rifiutata, catch viene eseguita, il che restituisce un valore: la catena ora è resolved, si arriva a then e viene registrato il valore.
    • Lo stato e il valore di promise1 saranno rejected e "Nope!".
    • Lo stato e il valore di promise2 saranno resolved e "done";

finally

A volte vuoi eseguire del codice dopo che una promise si è conclusa, indipendentemente dal fatto che si risolva o venga rifiutata. Dato promise.finally(onSettled), la callback onSettled non riceve nulla. Questa restituirà sempre una nuova promise «concatenata».

Restituire un value da finally copia lo stato e il valore dalla promise originale, ignorando il value. Lanciare un reason in finally rifiuta la promise «concatenata», sovrascrivendo qualsiasi stato, valore o motivo della promise originale.

Esempio

Diversi metodi usati insieme:

const myPromise = new Promise(function (resolve, reject) {
  const sampleData = [2, 4, 6, 8];
  const randomNumber = Math.round(Math.random() * 5);

  if (sampleData[randomNumber]) {
    resolve(sampleData[randomNumber]);
  } else {
    reject('Sampling did not result in a sample');
  }
});

const finalPromise = myPromise
  .then(function (sampled) {
    // If the random number was 0, 1, 2, or 3, this will be
    // reached and the number 2, 4, 6, or 8 will be logged.
    console.log(`Sampled data: ${sampled}`);
    return 'yay';
  })
  .catch(function (reason) {
    // If the random number was 4 or 5, this will be reached and
    // reason will be "An error occurred". The entire chain will
    // then reject with an Error with the reason as message.
    throw new Error(reason);
  })
  .finally(function () {
    // This will always log after either the sampled data is
    // logged or the error is raised.
    console.log('Promise completed');
  });
  • Nei casi in cui randomNumber è 0-3:
    • myPromise sarà risolta con il valore 2, 4, 6, or 8
    • finalPromise sarà risolta con il valore 'yay'
    • Ci saranno due registrazioni:
      • Sampled data: ...
      • Promise completed
  • Nei casi in cui randomNumber è 4-5:
    • myPromise sarà rifiutata con il motivo 'Sampling did not result in a sample'
    • finalPromise sarà rifiutata con il motivo Error('Sampling did not result in a sample')
    • Ci sarà una registrazione:
      • Promise completed
      • in alcuni ambienti questo produrrà una registrazione "uncaught rejected promise: Error('Sampling did not result in a sample')"

Come mostrato sopra, reject funziona con una stringa, e una promise può anche essere rifiutata con un Error.

Note

Se concatenare le promise o il loro uso generale non ti è chiaro, il tutorial su MDN è una buona risorsa da consultare.

Istruzioni

In questo esercizio fornirai un TranslationService che offre servizi di traduzione di base agli utenti gratuiti e traduzioni avanzate agli utenti premium con garanzie di qualità.

L'API

Hai trovato un'API di traduzione spaziale che soddisfa qualsiasi request di traduzione in un tempo ragionevole. Vuoi trarne vantaggio. I traduttori spaziali sono estremamente capricciosi e odiano le ripetizioni, quindi forniscono anche dei satelliti di archivio API dove puoi recuperare con fetch le traduzioni passate senza disturbarli.

Recuperare una traduzione

api.fetch(text) recupera una traduzione di text dall'archivio API e restituisce una promise che fornisce due valori:

  • translation: la traduzione effettiva
  • quality: la qualità espressa come numero

Se una traduzione non viene trovata nell'archivio API, l'API lancia un errore NotAvailable. Le traduzioni possono essere aggiunte usando il metodo api.request. Se 'text' non è traducibile, l'API lancia un errore Untranslatable.

api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })

Richiedere una traduzione

Alcune traduzioni esistono sicuramente, ma non sono ancora state aggiunte all'archivio API. Questa è la differenza tra NotAvailable (non presente nell'archivio, ma richiedibile) e Untranslatable (non traducibile).

api.request(text, callback) richiede che venga eseguita una traduzione di text e che venga aggiunta all'archivio API. Al termine viene chiamata la funzione callback.

  • In caso di successo, a callback viene passato undefined: questo indica che la traduzione è andata a buon fine ed è accessibile usando il metodo api.fetch.
  • In caso di errore, a callback viene passato un error: questo indica che qualcosa è andato storto. L'API spaziale è instabile, il che significa che fallisce spesso. Se succede, va bene richiamare api.request.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Attenzione! ⚠

Caution

L'API fa la sua magia teletrasportando i vari traduttori quando arriva una request. È un'azione molto costosa, quindi non va chiamata quando una traduzione è disponibile. Purtroppo non tutti leggono il manuale, quindi c'è un sistema per espellere chi si comporta male.

Se si chiama api.request per un text che è disponibile, l'API lancia un AbusiveClientError per questa chiamata, e per ogni chiamata successiva. Assicurati di non richiedere mai una traduzione se qualcosa è già stato tradotto.

1. Recuperare una traduzione, ignorando la qualità

Il servizio gratuito fornisce solo le traduzioni attualmente presenti nell'archivio API.

Implementa un metodo free(text) che fornisce agli utenti gratuiti la traduzione già presente nell'archivio API. Ignora la qualità e inoltra eventuali errori lanciati dall'API.

  • Restituisce la traduzione se può essere recuperata, indipendentemente dalla sua qualità
  • Inoltra qualsiasi errore dall'API di traduzione
  • Usa il metodo api.fetch (api.fetch restituisce una promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")

2. Recuperare una serie di traduzioni, tutto o niente

Implementa un metodo batch([text, text, ...]) per gli utenti gratuiti che traduce un array di testi usando il servizio gratuito, restituendo tutte le traduzioni, o un singolo errore.

  • Si risolve con tutte le traduzioni (nello stesso ordine), se sono tutte disponibili
  • Viene rifiutata con il primo errore incontrato
  • Viene rifiutata con un errore BatchIsEmpty se non viene fornito alcun testo
service.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]

service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")

service.batch([]);
// => Promise<...> rejects BatchIsEmpty()

3. Richiedere una traduzione, riprovando al massimo 2 volte

Implementa un metodo request(text) per gli utenti premium, che richiede che una traduzione venga aggiunta all'archivio API. La richiesta deve riprovare automaticamente se si verifica un errore. Non deve effettuare più di 3 chiamate per la stessa richiesta (non far arrabbiare i traduttori spaziali!!!).

  • Se api.request non restituisce un errore, risolvi con undefined
  • Se api.request restituisce un errore, riprova al massimo due volte
  • Se esaurisci i tentativi, rifiuta con l'ultimo errore ricevuto
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Recuperare una traduzione, ispezionarne la qualità o richiederla

Implementa un metodo premium(text, quality) per gli utenti premium che recupera una traduzione. Se una traduzione è NotAvailable, richiedila e recuperala dopo che è stata aggiunta all'archivio API. Il metodo deve restituire la traduzione solo se soddisfa una certa soglia di quality.

  • Se api.fetch si risolve, controlla la qualità prima di risolvere
  • Se api.fetch viene rifiutata, richiedi invece la traduzione
  • Se api.request viene rifiutata, inoltra l'errore
service.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."

service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()

service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"

N.B.

Note

The correct translation of 'arlogh Qoylu'pu'? is How many times has it been heard?.

Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
JavaScript Exercism

Vuoi iniziare Servizio di traduzione?

Iscriviti a Exercism per imparare e padroneggiare JavaScript con 37 concetti159 esercizi e il mentoring di persone reali, tutto gratis.