Tracks
/
JavaScript
JavaScript
/
Übungen
/
Übersetzungsdienst
Übersetzungsdienst

Übersetzungsdienst

Lernübung

Einführung

Das Objekt Promise steht für den schließlichen Abschluss (oder das Scheitern) einer asynchronen Operation und ihres resultierenden Werts.

Note

Dies ist für viele ein schwieriges Thema, besonders wenn du Programmieren in einer Sprache gelernt hast, die völlig synchron ist. Wenn du dich überfordert fühlst oder mehr über Nebenläufigkeit und Parallelität erfahren möchtest, dann schau ihn dir über go.dev an oder schau ihn dir direkt auf Vimeo an und lies die Folien zu dem brillanten Vortrag „Concurrency is not parallelism".

Der Lebenszyklus eines Promises

Ein Promise hat drei Zustände:

  1. pending
  2. fulfilled
  3. rejected

Wenn es erstellt wird, ist ein Promise pending. Irgendwann in der Zukunft kann es aufgelöst oder abgelehnt werden. Sobald ein Promise einmal aufgelöst oder abgelehnt wurde, kann es nie wieder aufgelöst oder abgelehnt werden, und sein Zustand kann sich auch nicht mehr ändern.

Mit anderen Worten:

  1. Wenn ein Promise pending ist:
    • kann es in den Zustand fulfilled oder rejected wechseln.
  2. Wenn ein Promise fulfilled ist:
    • darf es nicht in einen anderen Zustand wechseln.
    • muss es einen Wert haben, der sich nicht ändern darf.
  3. Wenn ein Promise rejected ist:
    • darf es nicht in einen anderen Zustand wechseln.
    • muss es einen Grund haben, der sich nicht ändern darf.

Ein Promise auflösen

Ein Promise kann auf verschiedene Arten aufgelöst werden:

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

In den Beispielen oben kann value alles sein, auch ein Fehler, undefined, null oder ein weiteres Promise. Normalerweise möchtest du mit einem Wert auflösen, der kein Fehler ist.

Ein Promise ablehnen

Ein Promise kann auf verschiedene Arten abgelehnt werden:

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

In den Beispielen oben kann reason alles sein, auch ein Fehler, undefined oder null. Normalerweise möchtest du mit einem Fehler ablehnen.

Promises verketten

Ein Promise kann mit einer zukünftigen Aktion fortgesetzt werden, sobald es aufgelöst oder abgelehnt wird.

then

Jedes Promise ist „thenable". Das heißt, es gibt eine Funktion then, die ausgeführt wird, sobald das ursprüngliche Promise aufgelöst wird. Mit promise.then(onResolved) erhält der Callback onResolved den Wert, mit dem das ursprüngliche Promise aufgelöst wurde. Dies gibt immer ein neues „verkettetes" Promise zurück.

Wenn du einen value aus then zurückgibst, wird das „verkettete" Promise aufgelöst. Wenn du einen reason in then wirfst, wird das „verkettete" Promise abgelehnt.

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

Dies gibt nach ungefähr 1000 ms "Success!" aus. Der Zustand und der Wert von promise1 sind resolved und "Success!". Der Zustand und der Wert von promise2 sind resolved und true.

Es gibt ein zweites Argument, das ausgeführt wird, wenn das ursprüngliche Promise abgelehnt wird. Mit promise.then(onResolved, onRejected) erhält der Callback onResolved den Wert, mit dem das ursprüngliche Promise aufgelöst wurde, oder der Callback onRejected erhält den Grund, aus dem das Promise abgelehnt wurde.

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 etwa der Hälfte der Fälle gibt dies nach ungefähr 1000 ms "Success!" aus.
    • Der Zustand und der Wert von promise1 sind resolved und "Success!".
    • Der Zustand und der Wert von promise2 sind resolved und true.
  • In etwa der Hälfte der Fälle gibt dies sofort "NOPE!" aus.
    • Der Zustand und der Wert von promise1 sind rejected und Nope!.
    • Der Zustand und der Wert von promise2 sind resolved und false.

Es ist wichtig zu verstehen, dass aufgrund der Regeln des Lebenszyklus das resolve, das etwa 1000 ms später kommt, stillschweigend ignoriert wird, wenn es rejectt, da sich der interne Zustand nicht mehr ändern kann, sobald er abgelehnt oder aufgelöst wurde. Es ist wichtig zu verstehen, dass das Zurückgeben eines Werts aus einem Promise dieses auflöst und das Werfen eines Werts es ablehnt. Wenn promise1 aufgelöst wird und ein verkettetes onResolved vorhanden ist: then(onResolved), dann ist diese Fortsetzung ein neues Promise, das aufgelöst oder abgelehnt werden kann. Wenn promise1 abgelehnt wird, aber ein verkettetes onRejected vorhanden ist: then(, onRejected), dann ist diese Fortsetzung ein neues Promise, das aufgelöst oder abgelehnt werden kann.

catch

Manchmal möchtest du Fehler abfangen und nur fortfahren, wenn das ursprüngliche Promise rejectt. Mit promise.catch(onCatch) erhält der Callback onCatch den Grund, aus dem das ursprüngliche Promise abgelehnt wurde. Dies gibt immer ein neues „verkettetes" Promise zurück.

Wenn du einen value aus catch zurückgibst, wird das „verkettete" Promise aufgelöst. Wenn du einen reason in catch wirfst, wird das „verkettete" Promise abgelehnt.

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 etwa der Hälfte der Fälle gibt dies nach ungefähr 1000 ms "Success!" aus. In der anderen Hälfte der Fälle gibt dies sofort 42 aus.

  • Wenn promise1 aufgelöst wird, wird catch übersprungen, der Aufruf erreicht then und gibt den Wert aus.
    • Der Zustand und der Wert von promise1 sind resolved und "Success!".
    • Der Zustand und der Wert von promise2 sind resolved und "done";
  • Wenn promise1 abgelehnt wird, wird catch ausgeführt, das einen Wert zurückgibt, und damit ist die Kette nun resolved und erreicht then und gibt den Wert aus.
    • Der Zustand und der Wert von promise1 sind rejected und "Nope!".
    • Der Zustand und der Wert von promise2 sind resolved und "done";

finally

Manchmal möchtest du Code ausführen, nachdem ein Promise abgeschlossen ist, egal ob es aufgelöst oder abgelehnt wird. Mit promise.finally(onSettled) erhält der Callback onSettled nichts. Dies gibt immer ein neues „verkettetes" Promise zurück.

Wenn du einen value aus finally zurückgibst, werden Zustand und Wert des ursprünglichen Promises kopiert, wobei der value ignoriert wird. Wenn du einen reason in finally wirfst, wird das „verkettete" Promise abgelehnt, wodurch ein eventueller Zustand, Wert oder Grund des ursprünglichen Promises überschrieben wird.

Beispiel

Mehrere dieser Methoden zusammen:

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');
  });
  • In den Fällen, in denen randomNumber 0-3 ist:
    • myPromise wird mit dem Wert 2, 4, 6, or 8 aufgelöst
    • finalPromise wird mit dem Wert 'yay' aufgelöst
    • Es gibt zwei Log-Ausgaben:
      • Sampled data: ...
      • Promise completed
  • In den Fällen, in denen randomNumber 4-5 ist:
    • myPromise wird mit dem Grund 'Sampling did not result in a sample' abgelehnt
    • finalPromise wird mit dem Grund Error('Sampling did not result in a sample') abgelehnt
    • Es gibt eine Log-Ausgabe:
      • Promise completed
      • in manchen Umgebungen führt dies zu einer Log-Ausgabe "uncaught rejected promise: Error('Sampling did not result in a sample')"

Wie oben gezeigt, funktioniert reject mit einem String, und ein Promise kann auch mit einem Error ablehnen.

Note

Wenn das Verketten von Promises oder die allgemeine Verwendung unklar ist, ist das Tutorial auf MDN eine gute Ressource zum Nachlesen.

Anleitung

In dieser Übung implementierst du einen TranslationService, der kostenlosen Mitgliedern grundlegende Übersetzungsdienste bietet und Premium-Mitgliedern erweiterte Übersetzungen mit Qualitätsgarantie.

Die API

Du hast eine Übersetzungs-API aus dem Weltraum gefunden, die jede Übersetzungs-request in angemessener Zeit erfüllt. Das möchtest du dir zunutze machen. Die Weltraumübersetzer sind extrem launisch und hassen Redundanz. Deshalb betreiben sie auch API-Speicher-Satelliten, aus denen du vergangene Übersetzungen fetchen kannst, ohne sie zu belästigen.

Eine Übersetzung abrufen

api.fetch(text) ruft eine Übersetzung von text aus dem API-Speicher ab und gibt ein promise zurück, das zwei Werte liefert:

  • translation: die eigentliche Übersetzung
  • quality: die Qualität als Zahl

Wenn eine Übersetzung nicht im API-Speicher gefunden wird, wirft die API einen NotAvailable-Fehler. Übersetzungen können mit der Methode api.request hinzugefügt werden. Wenn 'text' nicht übersetzbar ist, wirft die API einen Untranslatable-Fehler.

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

Eine Übersetzung anfordern

Manche Übersetzungen existieren sicher, wurden aber noch nicht zum API-Speicher hinzugefügt. Das ist der Unterschied zwischen NotAvailable (nicht im Speicher, aber anforderbar) und Untranslatable (nicht übersetzbar).

api.request(text, callback) fordert an, dass eine Übersetzung von text durchgeführt und in den API-Speicher aufgenommen wird. Nach Abschluss wird die Funktion callback aufgerufen.

  • Bei Erfolg wird callback undefined übergeben: Das bedeutet, dass die Übersetzung erfolgreich war und über die Methode api.fetch abgerufen werden kann.
  • Bei einem Fehler wird callback ein error übergeben: Das bedeutet, dass etwas schiefgelaufen ist. Die Weltraum-API ist instabil, das heißt, die API schlägt oft fehl. Wenn das passiert, ist es in Ordnung, api.request erneut aufzurufen.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Warnung! ⚠

Caution

Die API vollbringt ihre Magie, indem sie die verschiedenen Übersetzer herbeiteleportiert, wenn eine request eingeht. Das ist eine sehr kostspielige Aktion, deshalb sollte sie nicht aufgerufen werden, wenn eine Übersetzung verfügbar ist. Leider liest nicht jeder das Handbuch, deshalb gibt es ein System, um schlechte Akteure rauszuwerfen.

Wenn ein api.request für einen text aufgerufen wird, der verfügbar ist, wirft die API für diesen Aufruf einen AbusiveClientError, und für jeden weiteren Aufruf danach. Stelle sicher, dass du niemals eine Übersetzung anforderst, wenn schon etwas übersetzt wurde.

1. Rufe eine Übersetzung ab und ignoriere die Qualität

Der kostenlose Dienst bietet nur Übersetzungen, die sich gerade im API-Speicher befinden.

Implementiere eine Methode free(text), die kostenlosen Mitgliedern Übersetzungen liefert, die bereits im API-Speicher vorhanden sind. Ignoriere die Qualität und reiche alle von der API geworfenen Fehler weiter.

  • Gibt die Übersetzung zurück, wenn sie abgerufen werden kann, unabhängig von ihrer Qualität
  • Reicht jeden Fehler der Übersetzungs-API weiter
  • Verwendet die Methode api.fetch (api.fetch gibt ein promise zurück)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

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

2. Rufe einen Stapel Übersetzungen ab, alles oder nichts

Implementiere eine Methode batch([text, text, ...]) für kostenlose Mitglieder, die ein Array von Texten mit dem kostenlosen Dienst übersetzt und entweder alle Übersetzungen oder einen einzelnen Fehler zurückgibt.

  • Löst mit allen Übersetzungen auf (in derselben Reihenfolge), wenn alle verfügbar sind
  • Lehnt mit dem ersten auftretenden Fehler ab
  • Lehnt mit einem BatchIsEmpty-Fehler ab, wenn keine Texte angegeben werden
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. Fordere eine Übersetzung an und versuche es höchstens 2-mal erneut

Implementiere eine Premium-Methode request(text), die anfordert, dass eine Übersetzung zum API-Speicher hinzugefügt wird. Die Anfrage sollte bei einem Fehler automatisch erneut versucht werden. Sie sollte nicht mehr als 3 Aufrufe für dieselbe Anfrage ausführen (ärgere die Weltraumübersetzer nicht!!!).

  • Wenn api.request keinen Fehler zurückgibt, löse mit undefined auf
  • Wenn api.request einen Fehler zurückgibt, versuche es höchstens zweimal erneut
  • Wenn keine Versuche mehr übrig sind, lehne mit dem zuletzt erhaltenen Fehler ab
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Rufe eine Übersetzung ab, prüfe die Qualität oder fordere sie an

Implementiere eine Premium-Methode premium(text, quality), um eine Übersetzung abzurufen. Wenn eine Übersetzung NotAvailable ist, fordere die Übersetzung an und rufe sie ab, nachdem sie zum API-Speicher hinzugefügt wurde. Die Methode sollte die Übersetzung nur zurückgeben, wenn sie einen bestimmten quality-Schwellenwert erfüllt.

  • Wenn api.fetch auflöst, prüfe die Qualität, bevor du auflöst
  • Wenn api.fetch ablehnt, fordere stattdessen die Übersetzung an
  • Wenn api.request ablehnt, reiche den Fehler weiter
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?"

Hinweis

Note

Die korrekte Übersetzung von 'arlogh Qoylu'pu'? ist How many times has it been heard?.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
JavaScript Exercism

Bereit, mit Übersetzungsdienst zu starten?

Melde dich bei Exercism an, um JavaScript mit 37 Konzepte159 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.