allan ebdrup bloghoved ny

Generators i ES6 kan være godt nyt for node.js og asynkron JavaScript

Næste version af programmeringssproget JavaScript ES6, også kendt under kodenavnet harmony, indeholder rigtig mange forbedringer. En af dem er generators, som rummer nogle interessante muligheder for asynkron JavaScript-kode.

Kort introduktion til generators

Generators er en speciel ny type funktion. Generator-funktionen er en slags constructor.

At kalde generator-funktionen giver dig et objekt, der kan generere værdier, en værdi ad gangen, efterhånden som du beder om den næste værdi.

Det kunne for eksempel være en generator, der giver dig heltallene fra 1 og opefter (1, 2, 3, 4, 5, 6, ...):

function* heltalGenerator(){
  var n = 1;
 while (true){
      yield n++;
 }
}

Læg mærke til stjernen * efter function. Stjernen gør funktionen til en generator, og dermed får du muligheden for at bruge yield-kommandoen.

Du kalder generator-funktionen som en constructor, det giver dig et generator-objekt. Generator-objektet har metoden next(), som du kalder for at få den næste værdi i rækken:

var heltal = heltalGenerator();
console.log(heltal.next()); //udskriver: { value: 1, done: false }
console.log(heltal.next()); //udskriver: { value: 2, done: false }
console.log(heltal.next()); //udskriver: { value: 3, done: false }

Første gang du kalder next() svarer det til, at heltalGenerator-funktionen bliver kørt som en normal JavaScript-funktion.

Det nye er, at heltalGenerator har en yield-kommando.
Når funktionens eksekvering ramme yield-kommandoen, bliver værdien der yield-es returneret som value i det objekt, som du får tilbage, når du kalder next(). Herefter er generatoren "sat på pause" på det punkt i koden, hvor yield blev kaldt.

Næste gang next() kaldes, genoptager heltalGenerator-funktionen eksekveringen fra det sted den blev sat på pause. Alstå eksekveringen genoptages det sted, hvor heltalGenerator kaldte yield. Reultatet er, at heltalGenerator-funktionen yield-er n og tæller den en op hver gang next() kaldes, og dermed generer tallene 1, 2, 3, 4, ...

Hvis nu vi omskriver heltalGenerator-funktionen en smule:

function* heltalGenerator(){
  var n = 1;
 while (true){
      var nyStart = yield n++;
       n = nyStart || n;
  }
}

... kan vi tildele variablen nyStart en værdi, når vi kalder next(). Dermed kan vi få generatoren til at genere tal fra et nyt startpunkt:

var heltal = heltalGenerator();
console.log(heltal.next()); //udskriver: { value: 1, done: false }
console.log(heltal.next()); //udskriver: { value: 2, done: false }
console.log(heltal.next(5)); //udskriver: { value: 5, done: false }
console.log(heltal.next()); //udskriver: { value: 6, done: false }
console.log(heltal.next()); //udskriver: { value: 7, done: false }

Der hvor generator-funktionen blev "sat på pause" af at kalde yield, kan vi sende en værdi tilbage i funktionen, til når den genoptager sin eksekvering.

Som du måske kan se, kan generators bruges til mange spændende ting. Lad os kigge på en måde at bruge dem på i forhold til asynkrone kald.

Asynkrone IO-kald

Lad os først kigge på et meget forsimplet eksempel:

databaseOpslag(function(result){
  console.log(result);
});
 
//Denne funktion skal symbolisere noget asynkront I/O,
//typisk en HTTP request eller et database-opslag
function databaseOpslag(callback){
  setTimeout(function(){
     callback({ db: 'ok' }); 
 }, 100);
}

Resultatet af at køre kode er, at der efter 100 millisekunder udskrives:

{ db: 'ok' }

De første tre linjer er de vigtige i eksemplet. Jeg kalder den asynkrone databaseOpslag-funktion, og giver den en callback-funktion, som skal kaldes når databaseOpslag-funktionen er færdig med I/O-operationen og har et resultat klar.

Det er et meget brugt pattern i JavaScript. Både i browseren og på serveren i node.js. Nogle gange er det lavet med promises eller andre måder at lave flow-control. I node.js er det mest almindelige at bruge callbacks, som i eksemplet.

Problemet

Problemerne med denne måde at skrive sin kode er blandt andet:

  • Den "støj" det giver i koden at du skal definere en callback-funktion. Function er et langt keyword, og der skal også skrives paranteser og tuborg-klammer.
  • Callbacks giver et niveau af indentering. Koden rykkes et niveau ind med tab eller spaces. Dette kan delvist afhjælpes med function-hoisting, men ikke helt.
  • Når der tilføjes fejlhåndtering i hver callback, giver det endnu mere støj i koden. Dette kan afhjælpes en del med fx promises, eller andre former for flow-control-hjælpere. Jeg er ikke særlig vild med nogen af dem. Blandt andet på grund af det simple faktum, at stort set alle node.js-moduler bruger callbacks.
  • Hvis det logiske flow i koden kompliceres af forgreninger i if-else-statements, kan koden blive meget svær at læse. Og her er det ligemeget om du bruger callbacks, promises eller andre flow-control-libraries. Der er simpelthen for meget syntaktisk "støj" i koden, i alle de løsninger jeg har set.

Generators brugt til at gøre kode med asynkrone kald mere læsevenlig

Generators giver en spændende ny mulighed for at takle problemerne beskrevet ovenfor.

run((function* code(resume){
  var res = yield databaseOperation(resume);
 console.log(res);
}));
 
function run(generatorFunction){
   var generator = generatorFunction(resume);
 return generator.next();
 
  function resume(result){
       generator.next(result);
    }
}
 
function databaseOperation(callback){
 setTimeout(function(){
     callback({ db: 'ok' });
  }, 100);
}

Forklaring af koden:

  • run-funktionen fungerer på en sådan måde, at den generator-funktion run kaldes med som parameter, forventes at lave asynkrone operationer. I dette eksemplel er det code-funktionen der laver den asynkrone operation, nemlig database-kaldet.
  • code-funktionen skal yield-e når den laver den asynkrone operation i kaldet til databaseOperation og bliver dermed sat på pause.
  • code-funktionen sørger for at bruge resume-funktionen som den callback-funktion der skal kaldes når den asynkrone databaseOperation er færdig og har resultatet klar.
  • resume-funktionen kalder next() med resultatet af den asynkrone operation, og sender dermed resultatet tilbage ind i code-funktionen, der hvor den blev "sat på pause"

Resultatet af at køre kode er igen, at der efter 100 millisekunder udskrives:

{ db: 'ok' }

Slutresultatet er at den oprindelige kode:

databaseOpslag(function(result){
  console.log(result);
});

... nu kan skrives som:

run((function* code(resume){
  var res = yield databaseOperation(resume);
 console.log(res);
}));

Det virker måske ikke så imponerende. Men hvis vi nu forestiller os en funktion med fire databaseoperationer, der skal udføres serielt efter hinanden, fordi resultatet af hver database-operation skal bruges til den videre behandling:

databaseOperation(function(resultat1){
    console.log(resultat1);
    databaseOperation(function(resultat2){
     console.log(resultat2);
        databaseOperation(function(resultat3){
         console.log(resultat3);
            databaseOperation(function(resultat4){
             console.log(resultat4);
            });
        });
    });
});

... så kan vi bruge run til at skrive det samme sådan:

run((function* (resume){
  var resultat1 = yield databaseOperation(resume);
   console.log(resultat1);
    var resultat2 = yield databaseOperation(resume);
   console.log(resultat2);
    var resultat3 = yield databaseOperation(resume);
   console.log(resultat3);
    var resultat4 = yield databaseOperation(resume);
   console.log(resultat4);
}));

.. så kan du måske ane nogle muligheder?

Discalimer

Koden ovenfor er kun ment som meget forsimplede eksempler. Koden skal illustrere generators og konceptet med at bruge dem til asynkrone kald. Det er på ingen måde ment som kode, der skal bruges til noget som helst andet end at illustrere disse koncepter.

Hvis du vil se en produktions-klar implementation af konceptet, kan du kigge på suspend-modulet, som jeg synes har den bedste implementation. Suspend-modulets implementation har også håndtering af fejl, kan bruges med asynkrone kald med promises eller thunks og kan håndtere at køre flere asynkrone kald parallelt.

Et andet populært modul er co.

Jeg siger ikke, at du skal til at skrive al din asynkrone kode med det pattern beskrevet ovenfor. Jeg bruger det ikke selv (endnu). Dette er bare et debat-oplæg.

Hvornår kan jeg bruge ES6?

I første omgang har ES6 nok størst interesse for programmører der bruger JavaScript på serveren i node.js. Det kommer til at tage mange år, før du kan regne med, at alle dine brugeres browsere understøtter de nye features. Der er dog en mulighed for at bruge ES6-features i din kode i dag, og samtidigt sikre at dit website virker i browsere der kun har ES5. Du kan bruge traceur-kompileren der kompilerer ES6 til ES5.

For at bruge ES6 i node.js kræver det at du kører den ikke-produktionsmodne version 0.11 af node.js (tag den nyeste).

Du skal køre node.js med kommandoline-parameteren --harmony. Det er sådan, jeg har testet kode-eksemplerne i denne blogpost.

Jeg vil tro, at generators er med i version 0.12 af node.js, som er den næste stable release.

ES6 har meget andet spændende

Hvad bruger du til flow-control i din JavaScript-kode?

ES6 har mange andre spændende nye features. Har du en favorit?

Kommentarer (8)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
#1 Michael Zedeler

...og har det helt fint med CoffeeScript, er der jo Iced CoffeeScript som har noget der ligner corutiner indbygget i form af nøgleordene defer og await.

http://maxtaco.github.io/coffee-script/

Men ellers plejer jeg at leve med jQuery.Deferred() og vil i mangel af dette kigge efter noget Promise/A-kompatibelt (ja, jeg ved godt at jQuerys promise-implementation er flaky, men det er til at leve med).

Man kan undgå de dybe trappestrukturer med then().

  • 0
  • 1
#2 Allan Ebdrup Blogger

Du behøver som sagt ikke at vente på ES6, hvis du kompilerer til ES5. Hvis du alligevel har tænkt dig at indføre en compiler, ville jeg personligt gå efter, at holde mig til standarderne. Har du selv erfaringer med Iced CoffeScript?

De dybe trappestrukture undgår jeg som regel med function hoisting i min node.js.

  • 0
  • 0
#3 Michael Zedeler

Ikke endnu, men vi overvejer at fjerne jQuery samt jQuery Mobile fra LYT (http://github.com/Notalib/LYT) og erstatte det med AngularJS. I den forbindelse kan det være at vi vælger at bruge Iced CoffeeScript for at få en erstatning af Deferred() fra jQuery.

Det væsentligste kriterie for mit vedkommende er nok om man kan håndtere fejlsituationer på en elegant måde. Jeg har hverken haft tid til at undersøge hvordan det gøres med Generators eller Iced CoffeeScript.

I øvrigt ser det ud til at Traceur har en implementation af ES6-promises (som har await til fælles med Iced CoffeeScript), der er en mere velkendt måde at løse problemet på.

  • 2
  • 0
#4 Allan Ebdrup Blogger

Det væsentligste kriterie for mit vedkommende er nok om man kan håndtere fejlsituationer på en elegant måde. Jeg har hverken haft tid til at undersøge hvordan det gøres med Generators eller Iced CoffeeScript.

Ja det kom jeg ikke omkring. Jeg er helt enig i at det er utrolig vigtigt med fejlhåndteringen. Suspend håndterer fejl ganske pænt. Du kan håndtere dem et sted i flowet, hvis du bare skal "boble" fejlen opad.

I øvrigt ser det ud til at Traceur har en implementation af ES6-promises (som har await til fælles med Iced CoffeeScript), der er en mere velkendt måde at løse problemet på.

Promises og await er først i ES7, men hvis du vil kompilere så får du dem jo. Der er nok en risiko for at de ændrer standarden inden den er klar. Men hellere bruge noget der er tæt på standarden, end at lægge alle sine æg i Iced CoffeScript-kurven.

  • 0
  • 0
#6 Deleted User

Jeg må indrømme at jeg i første omgang ikke så noget særligt i generators, de første eksempler er jo ret let implementeret med en closure i stedet.

Men du viser at det jo faktisk kan bruges til at sætte en tråd i stå.

Det er bare ærgerligt at funktionaliteten skal bruges på den bagvendte måde, det tog mig lidt for lang tid at forstå din kode til at det føles som en god løsning.

  • 0
  • 0
#7 Lars Tørnes Hansen

Jeg tænker at det kunne være muligt at en generator kunne laves med et closure: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures

... og rigtig nok. en hurtig søgning med teksten "javascript generator using a closure" (uden "-tegnene) giver f.eks. http://www.nixtu.info/2012/03/implementing-generators-in-javascript.html

Jeg skal lige bemærke at jeg absolut ikke er en haj ud i JavaScript, men sproget interesserer mig, fordi det bruges i browsere.

  • 0
  • 0
#8 Allan Ebdrup Blogger

Jeg er helt enig i at løsningen ikke er verdens mest elegante, og jeg synes heller ikke det er "godt nok".

Hvis vi vælger at bruge det, vil det nok være fordi det ville være et skridt på vejen mod await som er planlagt til ES7. Med håbet om, at det at omskrive en funktion der bruger run ville være nem at omskrive til await, når den kommer.

Brendan Eich har sagt, at det er planen der kun skal gå et år mellem nye versioner af ES. Så forhåbenlig vil man kunne bruge await, i en draft version, i node.js, om et års tid.

  • 0
  • 0
Log ind eller Opret konto for at kommentere