Треки
/
JavaScript
JavaScript
/
Вправи
/
Служба перекладу
Служба перекладу

Служба перекладу

Навчальна вправа

Вступ

Обʼєкт Promise представляє майбутнє завершення (або невдачу) асинхронної операції та її результуюче значення.

Note

Це складна тема для багатьох людей, особливо якщо ми знаємо програмування в мові, яка повністю синхронна. Якщо ми відчуваємо, що це занадто складно, або хочемо дізнатися більше про конкурентність і паралелізм, подивімося (через go.dev) або подивімося безпосередньо через vimeo і прочитаймо слайди блискучої доповіді «Concurrency is not parallelism».

Життєвий цикл проміса

Promise має три стани:

  1. очікування
  2. виконано
  3. відхилено

Коли проміс створюють, він перебуває в стані очікування. У якийсь момент у майбутньому він може виконатися або відхилитися. Щойно проміс виконався або відхилився, він уже ніколи не зможе виконатися чи відхилитися знову, і його стан теж не може змінитися.

Іншими словами:

  1. У стані очікування проміс:
    • може перейти або в стан «виконано», або в стан «відхилено».
  2. У стані «виконано» проміс:
    • не повинен переходити в жоден інший стан.
    • повинен мати значення, яке не повинно змінюватися.
  3. У стані «відхилено» проміс:
    • не повинен переходити в жоден інший стан.
    • повинен мати причину, яка не повинна змінюватися.

Виконання проміса

Проміс можна виконати в різні способи:

// 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 або іншим промісом. Зазвичай ми хочемо виконати проміс зі значенням, яке не є помилкою.

Відхилення проміса

Проміс можна відхилити в різні способи:

// 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.then() викликається, коли promise виконується
  • promise.catch() викликається, коли promise відхиляється
  • promise.finally() викликається, коли promise або виконується, або відхиляється

then

Кожен проміс є «thenable». Це означає, що в нього є доступна функція then, яка виконається, щойно початковий проміс виконається. Якщо маємо promise.then(onResolved), то функція зворотного виклику onResolved отримує значення, яким було виконано початковий проміс. Це завжди повертає новий проміс у ланцюжку.

Повернення value з then виконує проміс у ланцюжку. Якщо в then кинути reason, проміс у ланцюжку відхиляється.

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

Це виведе "Success!" приблизно через 1000 мс. Стан і значення promise1 будуть resolved і "Success!". Стан і значення promise2 будуть resolved і true.

Є й другий аргумент, який запускається, коли початковий проміс відхиляється. Якщо маємо promise.then(onResolved, onRejected), то функція зворотного виклику onResolved отримує значення, яким було виконано початковий проміс, або функція зворотного виклику onRejected отримує причину, з якої проміс було відхилено.

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);
  • Приблизно в половині випадків це виведе "Success!" приблизно через 1000 мс.
    • Стан і значення promise1 будуть resolved і "Success!".
    • Стан і значення promise2 будуть resolved і true.
  • Приблизно в половині випадків це одразу виведе "NOPE!".
    • Стан і значення promise1 будуть rejected і Nope!.
    • Стан і значення promise2 будуть resolved і false.

Важливо розуміти, що через правила життєвого циклу, коли відбувається reject, той resolve, який надходить приблизно через 1000 мс, мовчки ігнорується, бо внутрішній стан не може змінитися після відхилення чи виконання. Важливо розуміти, що повернення значення з проміса виконує його, а викидання значення відхиляє його. Коли promise1 виконується і є приєднаний onResolved: then(onResolved), то цей наступний крок стає новим промісом, який може виконатися або відхилитися. Коли promise1 відхиляється, але є приєднаний onRejected: then(, onRejected), то цей наступний крок стає новим промісом, який може виконатися або відхилитися.

catch

Іноді ми хочемо перехопити помилки й продовжити лише тоді, коли початковий проміс зазнає reject. Якщо маємо promise.catch(onCatch), то функція зворотного виклику onCatch отримує причину, з якої початковий проміс було відхилено. Це завжди повертає новий проміс у ланцюжку.

Повернення value з catch виконує проміс у ланцюжку. Якщо в catch кинути reason, проміс у ланцюжку відхиляється.

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

Приблизно в половині випадків це виведе "Success!" приблизно через 1000 мс. В іншій половині випадків це одразу виведе 42.

  • Якщо promise1 виконується, catch пропускається, керування доходить до then і значення виводиться в консоль.
    • Стан і значення promise1 будуть resolved і "Success!".
    • Стан і значення promise2 будуть resolved і "done";
  • Якщо promise1 відхиляється, виконується catch, який повертає значення, і таким чином ланцюжок тепер перебуває в стані resolved, керування доходить до then і значення виводиться в консоль.
    • Стан і значення promise1 будуть rejected і "Nope!".
    • Стан і значення promise2 будуть resolved і "done";

finally

Іноді ми хочемо виконати код після того, як проміс завершиться, незалежно від того, виконався він чи відхилився. Якщо маємо promise.finally(onSettled), то функція зворотного виклику onSettled не отримує нічого. Це завжди повертає новий проміс у ланцюжку.

Повернення value з finally копіює стан і значення початкового проміса, ігноруючи value. Якщо в finally кинути reason, проміс у ланцюжку відхиляється, перезаписуючи будь-який стан і значення чи причину початкового проміса.

Приклад

Кілька методів разом:

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'
    • Буде два виводи:
      • Sampled data: ...
      • Promise completed
  • У випадках, коли randomNumber дорівнює 4-5:
    • myPromise буде відхилено з причиною 'Sampling did not result in a sample'
    • finalPromise буде відхилено з причиною Error('Sampling did not result in a sample')
    • Буде один вивід:
      • Promise completed
      • у деяких середовищах це дасть вивід "uncaught rejected promise: Error('Sampling did not result in a sample')"

Як показано вище, reject працює з рядком тексту (англ. string), і проміс також може відхилитися з Error.

Note

Якщо ланцюжки промісів або загальне використання залишаються незрозумілими, варто звернутися до туторіалу на MDN.

Вказівки

У цій вправі ми створимо TranslationService, який надає базові послуги перекладу безплатним учасникам, а розширений переклад із гарантіями якості - преміум-учасникам.

API

Ми знайшли космічний API перекладу, який виконує будь-який request перекладу за прийнятний час. І ми хочемо цим скористатися. Космічні перекладачі вкрай примхливі й не терплять надмірності, тож вони також надають супутники сховища API, де можна fetch минулі переклади, не турбуючи їх.

Отримання перекладу

api.fetch(text) отримує переклад text зі сховища API і повертає promise, який надає два значення:

  • translation: власне переклад
  • quality: якість, виражена числом

Якщо перекладу немає в сховищі API, API викидає помилку NotAvailable. Переклади можна додавати за допомогою методу api.request. Якщо «text» перекласти неможливо, API викидає помилку Untranslatable.

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

Запит перекладу

Деякі переклади точно існують, але їх ще не додано до сховища API. Ось у чому різниця між NotAvailable (немає у сховищі, але можна запросити) і Untranslatable (перекласти неможливо).

api.request(text, callback) запитує виконання перекладу text і додавання його до сховища API. Після завершення викликається функція 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.

Реалізуйте метод free(text), який надає безплатним учасникам переклади, що вже є в сховищі API. Ігноруйте якість і передавайте далі будь-які помилки, які викидає 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. Запит перекладу з максимум двома повторними спробами

Реалізуйте метод request(text) для преміум-учасників, який запитує додавання перекладу до сховища API. Запит має автоматично повторюватися в разі невдачі. Він має виконати не більше ніж 3 виклики для одного запиту (не дратуйте космічних перекладачів!!!).

  • Якщо api.request не повертає помилку, завершуйте успішно з undefined
  • Якщо api.request повертає помилку, повторіть щонайбільше двічі
  • Якщо спроби вичерпано, відхиліть з останньою отриманою помилкою
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Отримання перекладу, перевірка якості або його запит

Реалізуйте метод premium(text, quality) для преміум-учасників, щоб отримувати переклад. Якщо переклад NotAvailable, запитайте переклад і отримайте його після додавання до сховища API. Метод має повертати переклад лише тоді, коли він відповідає певному порогу 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'? - Скільки разів це чули?.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
JavaScript Exercism

Час розпочати Служба перекладу?

Зареєструйтеся на Exercism, щоб вивчати й опановувати JavaScript, а також 37 концепцій159 вправ та справжнє наставництво від людей, і все це безкоштовно.