Διαδρομές
/
JavaScript
JavaScript
/
Ασκήσεις
/
Υπηρεσία Μετάφρασης
Υπηρεσία Μετάφρασης

Υπηρεσία Μετάφρασης

Άσκηση εκμάθησης

Εισαγωγή

Το αντικείμενο Promise αντιπροσωπεύει την τελική ολοκλήρωση (ή την αποτυχία) μιας ασύγχρονης λειτουργίας και την τιμή που προκύπτει από αυτή.

Note

Αυτό είναι ένα δύσκολο θέμα για πολλούς ανθρώπους, ειδικά αν ξέρεις προγραμματισμό σε μια γλώσσα που είναι εντελώς σύγχρονη. Αν νιώθεις να πνίγεσαι, ή αν θα ήθελες να μάθεις περισσότερα για concurrency και parallelism, δες (μέσω go.dev) ή δες απευθείας στο vimeo και διάβασε τις διαφάνειες της εξαιρετικής ομιλίας "Concurrency is not parallelism".

Ο κύκλος ζωής ενός Promise

Ένα Promise έχει τρεις καταστάσεις:

  1. pending
  2. fulfilled
  3. rejected

Όταν δημιουργείται, ένα Promise είναι pending. Κάποια στιγμή στο μέλλον μπορεί να resolve ή να reject. Μόλις ένα Promise γίνει resolved ή rejected μία φορά, δεν μπορεί ποτέ να γίνει resolved ή rejected ξανά, ούτε μπορεί να αλλάξει η κατάστασή του.

Με άλλα λόγια:

  1. Όταν είναι pending, ένα Promise:
    • μπορεί να μεταβεί είτε στην κατάσταση fulfilled είτε στην κατάσταση rejected.
  2. Όταν είναι fulfilled, ένα Promise:
    • δεν πρέπει να μεταβεί σε καμία άλλη κατάσταση.
    • πρέπει να έχει μια τιμή, η οποία δεν πρέπει να αλλάζει.
  3. Όταν είναι rejected, ένα Promise:
    • δεν πρέπει να μεταβεί σε καμία άλλη κατάσταση.
    • πρέπει να έχει έναν λόγο, ο οποίος δεν πρέπει να αλλάζει.

Η επίλυση ενός Promise

Ένα Promise μπορεί να επιλυθεί με διάφορους τρόπους:

// 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;
});

Στα παραπάνω παραδείγματα το value μπορεί να είναι οτιδήποτε, ακόμη και ένα σφάλμα, undefined, null ή ένα άλλο Promise. Συνήθως θέλεις να επιλύσεις το Promise με μια τιμή που δεν είναι σφάλμα.

Η απόρριψη ενός Promise

Ένα Promise μπορεί να απορριφθεί με διάφορους τρόπους:

// 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
})

Στα παραπάνω παραδείγματα ο reason μπορεί να είναι οτιδήποτε, ακόμη και ένα σφάλμα, undefined ή null. Συνήθως θέλεις να απορρίψεις το Promise με ένα σφάλμα.

Η αλυσιδωτή σύνδεση ενός Promise

Ένα Promise μπορεί να συνεχιστεί με μια μελλοντική ενέργεια μόλις επιλυθεί ή απορριφθεί.

  • Το promise.then() καλείται μόλις το promise επιλυθεί
  • Το promise.catch() καλείται μόλις το promise απορριφθεί
  • Το promise.finally() καλείται μόλις το promise είτε επιλυθεί είτε απορριφθεί

then

Κάθε Promise είναι "thenable". Αυτό σημαίνει ότι υπάρχει μια συνάρτηση then που θα εκτελεστεί μόλις επιλυθεί το αρχικό Promise. Δεδομένου του promise.then(onResolved), το callback onResolved λαμβάνει την τιμή με την οποία επιλύθηκε το αρχικό Promise. Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.

Η επιστροφή ενός value από το then επιλύει το αλυσιδωτό Promise. Το πέταγμα ενός reason στο then απορρίπτει το αλυσιδωτό Promise.

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;
});

Αυτό θα τυπώσει στο log το "Success!" μετά από περίπου 1000 ms. Η κατάσταση και η τιμή του promise1 θα είναι resolved και "Success!". Η κατάσταση και η τιμή του promise2 θα είναι resolved και true.

Υπάρχει και ένα δεύτερο όρισμα που εκτελείται όταν το αρχικό Promise απορριφθεί. Δεδομένου του promise.then(onResolved, onRejected), το callback onResolved λαμβάνει την τιμή με την οποία επιλύθηκε το αρχικό Promise, ή το callback onRejected λαμβάνει τον λόγο για τον οποίο απορρίφθηκε το Promise.

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);
  • Σε περίπου 1/2 των περιπτώσεων, αυτό θα τυπώσει στο log το "Success!" μετά από περίπου 1000 ms.
    • Η κατάσταση και η τιμή του promise1 θα είναι resolved και "Success!".
    • Η κατάσταση και η τιμή του promise2 θα είναι resolved και true.
  • Σε περίπου 1/2 των περιπτώσεων, αυτό θα τυπώσει αμέσως στο log το "NOPE!".
    • Η κατάσταση και η τιμή του promise1 θα είναι rejected και Nope!.
    • Η κατάσταση και η τιμή του promise2 θα είναι resolved και false.

Είναι σημαντικό να καταλάβεις ότι, λόγω των κανόνων του κύκλου ζωής, όταν κάνει reject, το resolve που έρχεται ~1000 ms αργότερα αγνοείται σιωπηλά, αφού η εσωτερική κατάσταση δεν μπορεί να αλλάξει από τη στιγμή που θα απορριφθεί ή θα επιλυθεί. Είναι σημαντικό να καταλάβεις ότι η επιστροφή μιας τιμής από ένα Promise το επιλύει, και το πέταγμα μιας τιμής το απορρίπτει. Όταν το promise1 επιλυθεί και υπάρχει συνδεδεμένο onResolved: then(onResolved), τότε αυτή η συνέχεια είναι ένα νέο Promise που μπορεί να επιλυθεί ή να απορριφθεί. Όταν το promise1 απορριφθεί αλλά υπάρχει συνδεδεμένο onRejected: then(, onRejected), τότε αυτή η συνέχεια είναι ένα νέο Promise που μπορεί να επιλυθεί ή να απορριφθεί.

catch

Μερικές φορές θέλεις να συλλάβεις σφάλματα και να συνεχίσεις μόνο όταν το αρχικό Promise απορριφθεί. Δεδομένου του promise.catch(onCatch), το callback onCatch λαμβάνει τον λόγο για τον οποίο απορρίφθηκε το αρχικό Promise. Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.

Η επιστροφή ενός value από το catch επιλύει το αλυσιδωτό Promise. Το πέταγμα ενός reason στο catch απορρίπτει το αλυσιδωτό Promise.

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);

Σε περίπου 1/2 των περιπτώσεων, αυτό θα τυπώσει στο log το "Success!" μετά από περίπου 1000 ms. Στις άλλες 1/2 των περιπτώσεων, αυτό θα τυπώσει αμέσως στο log το 42.

  • Αν το promise1 επιλυθεί, το catch παραλείπεται και φτάνει στο then, το οποίο τυπώνει την τιμή στο log.
    • Η κατάσταση και η τιμή του promise1 θα είναι resolved και "Success!".
    • Η κατάσταση και η τιμή του promise2 θα είναι resolved και "done";
  • Αν το promise1 απορριφθεί, το catch εκτελείται, το οποίο επιστρέφει μια τιμή, και έτσι η αλυσίδα πλέον είναι resolved, και φτάνει στο then, το οποίο τυπώνει την τιμή στο log.
    • Η κατάσταση και η τιμή του promise1 θα είναι rejected και "Nope!".
    • Η κατάσταση και η τιμή του promise2 θα είναι resolved και "done";

finally

Μερικές φορές θέλεις να εκτελέσεις κώδικα αφού ένα Promise ολοκληρωθεί, ανεξάρτητα από το αν επιλυθεί ή απορριφθεί. Δεδομένου του promise.finally(onSettled), το callback onSettled δεν λαμβάνει τίποτα. Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.

Η επιστροφή ενός value από το finally αντιγράφει την κατάσταση και την τιμή από το αρχικό Promise, αγνοώντας το value. Το πέταγμα ενός reason στο finally απορρίπτει το αλυσιδωτό Promise, αντικαθιστώντας οποιαδήποτε κατάσταση και τιμή ή λόγο από το αρχικό Promise.

Παράδειγμα

Διάφορες από τις μεθόδους μαζί:

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');
  });
  • Στις περιπτώσεις που το randomNumber είναι 0-3:
    • Το myPromise θα επιλυθεί με την τιμή 2, 4, 6, or 8
    • Το finalPromise θα επιλυθεί με την τιμή 'yay'
    • Θα υπάρξουν δύο μηνύματα στο log:
      • Sampled data: ...
      • Promise completed
  • Στις περιπτώσεις που το randomNumber είναι 4-5:
    • Το myPromise θα απορριφθεί με τον λόγο 'Sampling did not result in a sample'
    • Το finalPromise θα απορριφθεί με τον λόγο Error('Sampling did not result in a sample')
    • Θα υπάρξει ένα μήνυμα στο log:
      • Promise completed
      • σε ορισμένα περιβάλλοντα αυτό θα παράγει ένα μήνυμα "uncaught rejected promise: Error('Sampling did not result in a sample')" στο log

Όπως φαίνεται παραπάνω, το reject δουλεύει με συμβολοσειρά, και ένα Promise μπορεί επίσης να απορριφθεί με ένα Error.

Note

Αν η αλυσιδωτή σύνδεση των Promises ή η γενικότερη χρήση τους δεν είναι ξεκάθαρη, ο οδηγός στο MDN είναι μια καλή πηγή για να διαβάσεις.

Οδηγίες

Σε αυτή την άσκηση, θα φτιάξεις μια TranslationService που παρέχει βασικές υπηρεσίες μετάφρασης σε δωρεάν μέλη και προηγμένη μετάφραση σε premium μέλη με εγγυήσεις ποιότητας.

Το API

Βρήκες ένα API μετάφρασης από το διάστημα που εκτελεί οποιοδήποτε request μετάφρασης σε λογικό χρόνο. Θέλεις να το εκμεταλλευτείς. Οι μεταφραστές του διαστήματος είναι εξαιρετικά ιδιότροποι και μισούν την επανάληψη, οπότε παρέχουν και δορυφόρους API storage όπου μπορείς να κάνεις fetch παλιές μεταφράσεις χωρίς να τους ενοχλήσεις.

Λήψη μιας μετάφρασης

Το api.fetch(text) ανακτά μια μετάφραση του text από το API storage και επιστρέφει ένα promise που παρέχει δύο τιμές:

  • translation: η ίδια η μετάφραση
  • quality: η ποιότητα εκφρασμένη ως αριθμός

Αν μια μετάφραση δε βρεθεί στο API storage, το API πετάει ένα σφάλμα NotAvailable. Οι μεταφράσεις μπορούν να προστεθούν με τη μέθοδο api.request. Αν το 'text' δε μπορεί να μεταφραστεί, το API πετάει ένα σφάλμα Untranslatable.

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

Ζήτηση μιας μετάφρασης

Κάποιες μεταφράσεις σίγουρα υπάρχουν, αλλά δεν έχουν προστεθεί ακόμα στο API storage. Αυτή είναι η διαφορά ανάμεσα στο NotAvailable (δεν είναι στο storage, αλλά μπορεί να ζητηθεί) και το Untranslatable (δε μπορεί να μεταφραστεί).

Το api.request(text, callback) ζητά να εκτελεστεί μια μετάφραση του text και να προστεθεί στο API storage. Όταν ολοκληρωθεί, καλείται η συνάρτηση callback.

  • Σε περίπτωση επιτυχίας, στο callback περνιέται το undefined: αυτό δείχνει ότι η μετάφραση ήταν επιτυχής και είναι προσβάσιμη με τη μέθοδο api.fetch.
  • Σε περίπτωση αποτυχίας, στο callback περνιέται ένα error: αυτό δείχνει ότι κάτι πήγε στραβά. Το API του διαστήματος είναι ασταθές, που σημαίνει ότι αποτυγχάνει συχνά. Αν συμβεί αυτό, δεν πειράζει να καλέσεις ξανά το api.request.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Προσοχή! ⚠

Caution

Το API κάνει τη μαγεία του τηλεμεταφέροντας τους διάφορους μεταφραστές όταν φτάνει ένα request. Αυτή είναι μια πολύ δαπανηρή ενέργεια, οπότε δεν πρέπει να καλείται όταν μια μετάφραση είναι διαθέσιμη. Δυστυχώς, δε διαβάζουν όλοι το εγχειρίδιο, γι' αυτό υπάρχει ένα σύστημα που αποβάλλει τους κακόβουλους χρήστες.

Αν κληθεί ένα api.request για text που είναι διαθέσιμο, το API πετάει ένα AbusiveClientError για αυτή την κλήση, και για κάθε επόμενη κλήση. Βεβαιώσου ότι ποτέ δε ζητάς μετάφραση αν κάτι έχει ήδη μεταφραστεί.

1. Ανάκτησε μια μετάφραση, αγνοώντας την ποιότητα

Η δωρεάν υπηρεσία παρέχει μόνο μεταφράσεις που βρίσκονται αυτή τη στιγμή στο API storage.

Υλοποίησε μια μέθοδο free(text) που παρέχει στα δωρεάν μέλη μεταφράσεις που υπάρχουν ήδη στο API storage. Αγνόησε την ποιότητα και προώθησε τυχόν σφάλματα που πετάει το API.

  • Επιστρέφει τη μετάφραση αν μπορεί να ανακτηθεί, ανεξάρτητα από την ποιότητά της
  • Προωθεί οποιοδήποτε σφάλμα από το API μετάφρασης
  • Χρησιμοποιεί τη μέθοδο api.fetch (το api.fetch επιστρέφει ένα promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

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

2. Ανάκτησε μια δέσμη μεταφράσεων, όλα ή τίποτα

Υλοποίησε μια μέθοδο batch([text, text, ...]) για δωρεάν μέλη που μεταφράζει έναν πίνακα κειμένων με τη δωρεάν υπηρεσία, επιστρέφοντας όλες τις μεταφράσεις ή ένα μόνο σφάλμα.

  • Επιλύεται με όλες τις μεταφράσεις (με την ίδια σειρά), αν είναι όλες διαθέσιμες
  • Απορρίπτεται με το πρώτο σφάλμα που θα προκύψει
  • Απορρίπτεται με σφάλμα BatchIsEmpty αν δε δοθούν κείμενα
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. Ζήτησε μια μετάφραση, με έως 2 επαναλήψεις

Υλοποίησε μια μέθοδο premium χρήστη request(text), που ζητά να προστεθεί μια μετάφραση στο API storage. Το αίτημα πρέπει να επαναλαμβάνεται αυτόματα αν προκύψει αποτυχία. Δεν πρέπει να κάνει περισσότερες από 3 κλήσεις για το ίδιο αίτημα (μην αναστατώσεις τους μεταφραστές του διαστήματος!!!).

  • Αν το api.request δεν επιστρέψει σφάλμα, επιλύεται με undefined
  • Αν το api.request επιστρέψει σφάλμα, επανάλαβε έως δύο φορές
  • Αν εξαντλήσεις τις επαναλήψεις, απορρίπτεται με το τελευταίο σφάλμα που ελήφθη
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Ανάκτησε μια μετάφραση, εξέτασε την ποιότητα ή ζήτησέ την

Υλοποίησε μια μέθοδο premium χρήστη premium(text, quality) για να ανακτήσεις μια μετάφραση. Αν μια μετάφραση είναι NotAvailable, ζήτησε τη μετάφραση και ανακτήσέ την αφού προστεθεί στο API storage. Η μέθοδος πρέπει να επιστρέφει τη μετάφραση μόνο αν πληροί ένα συγκεκριμένο όριο quality.

  • Αν το api.fetch επιλυθεί, έλεγξε την ποιότητα πριν επιλυθεί
  • Αν το api.fetch απορριφθεί, ζήτησε τη μετάφραση αντ' αυτού
  • Αν το api.request απορριφθεί, προώθησε το σφάλμα
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?"

Σημείωση

Note

Η σωστή μετάφραση του 'arlogh Qoylu'pu'? είναι How many times has it been heard?.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
JavaScript Exercism

Έτοιμος να ξεκινήσεις την άσκηση Υπηρεσία Μετάφρασης;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις JavaScript με 37 έννοιες159 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.