Generatory

> Dodaj do ulubionych

ES6 bez tajemnic to cykl artyku┼é├│w po┼Ťwi─Öconych nowym sk┼éadnikom j─Özyka JavaScript, kt├│re pojawi─ů si─Ö w 6. edycji standardu ECMAScript, w skr├│cie ES6.

Temat dzisiejszego artyku┼éu to co┼Ť, co mnie niezmiernie fascynuje. Powiemy sobie bowiem o najbardziej magicznym elemencie funkcjonalno┼Ťci w ES6.

Dlaczego ÔÇ×magicznymÔÇŁ? Po pierwsze r├│┼╝ni si─Ö on od innych sk┼éadnik├│w JS do tego stopnia, ┼╝e na pierwszy rzut oka przypomina jakie┼Ť tajemne zakl─Öcie. Mo┼╝na w┼éa┼Ťciwie powiedzie─ç, ┼╝e stawia JS na g┼éowie! Je┼Ťli to nie czary, to nie wiem jak to inaczej okre┼Ťli─ç.

Ponadto, dzi─Öki niemu mo┼╝emy znacznie upro┼Ťci─ç kod oraz zapomnie─ç o koszmarze wywo┼éa┼ä zwrotnych, co sprawia, ┼╝e mamy do czynienia wr─Öcz ze zjawiskami nadprzyrodzonymi.

Chyba ju┼╝ zaczynam przesadza─ç, prawda? Przejd┼║my wi─Öc do sedna, a przekonasz si─Ö o tym wszystkim sam.

Generatory w ES6 ÔÇô wprowadzenie

Czym s─ů generatory? Na pocz─ůtek sp├│jrzmy na przyk┼éad:

function* quips(name) {
  yield "witaj " + name + "!";
  yield "Mam nadziej─Ö, ┼╝e podobaj─ů ci si─Ö moje artyku┼éy";
  if (name.startsWith("X")) {
    yield "Ale fajnie, ┼╝e twoje imi─Ö zaczyna si─Ö na X, " + name;
  }
  yield "Do zobaczenia!";
}

Oto kod gadaj─ůcego kota, prawdopodobnie najwa┼╝niejszego obecnie rodzaju aplikacji internetowej. Gdy stwierdzisz, ┼╝e masz ju┼╝ w g┼éowie kompletny m─Ötlik, wr├│─ç do naszego artyku┼éu po wyja┼Ťnienie).

Przypomina funkcj─Ö, prawda? To specjalny rodzaj funkcji zwany generatorem. Cho─ç generatory maj─ů wiele wsp├│lnego z funkcjami, od razu mo┼╝na zauwa┼╝y─ç istniej─ůce pomi─Ödzy nimi dwie r├│┼╝nice:

  • Zwyk┼ée funkcje deklarowane s─ů za pomoc─ů s┼éowa kluczowego function, natomiast generatory za pomoc─ů wyra┼╝enia function*.
  • Wewn─ůtrz generatora yield jest s┼éowem kluczowym o sk┼éadni podobnej do instrukcji return. R├│┼╝nica polega na tym, ┼╝e instrukcja return mo┼╝e by─ç u┼╝yta wewn─ůtrz funkcji (nawet generatora) tylko raz. W przypadku instrukcji yield takich ogranicze┼ä nie ma. Wyra┼╝enie yield powoduje wstrzymanie wykonywania generatora, co umo┼╝liwia p├│┼║niejsze wznowienie jego dzia┼éania.

I to by było na tyle. Oto kluczowa różnica pomiędzy zwykłymi funkcjami a generatorami: wstrzymanie funkcji nie jest możliwe. Generatory natomiast wstrzymać można.

Jak dzia┼éaj─ů generatory

Co si─Ö stanie, je┼Ťli wywo┼éamy funkcj─Ö quips()?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "Witaj jorendorff!", done: false }
> iter.next()
  { value: "Mam nadziej─Ö, ┼╝e podobaj─ů ci si─Ö moje artyku┼éy", done: false }
> iter.next()
  { value: "Do zobaczenia!", done: false }
> iter.next()
  { value: undefined, done: true }

Przyzwyczai┼ée┼Ť si─Ö ju┼╝ pewnie do dzia┼éania zwyk┼éych funkcji. Standardowa funkcja uruchamia si─Ö zaraz po jej wywo┼éaniu, a jej dzia┼éanie dobiega ko┼äca w momencie wykonania instrukcji return b─ůd┼║ zg┼éoszenia wyj─ůtku. Dla programist├│w JS to zupe┼énie oczywiste.

Samo wywo┼éanie generatora wygl─ůda identycznie: quips("jorendorff"). Generator nie rozpoczyna jednak dzia┼éania od razu. Na pocz─ůtek zwracany jest wstrzymany obiekt generatora (w powy┼╝szym przyk┼éadzie nosi on nazw─Ö iter) ÔÇô mo┼╝na go por├│wna─ç do wstrzymanego wywo┼éania funkcji. Do wstrzymania tego dochodzi zaraz na pocz─ůtku generatora, tu┼╝ przed wykonaniem pierwszej linijki kodu.

Po każdym wywołaniu metody obiektu generatora .next() funkcja zostaje wznowiona i jest wykonywana aż do kolejnej instrukcji yield.

Z tego powodu za ka┼╝dym razem po wywo┼éaniu metody iter.next() otrzymywali┼Ťmy inn─ů warto┼Ť─ç ┼éa┼äcuchow─ů. Warto┼Ťci zwracane s─ů przez instrukcj─Ö yield znajduj─ůc─ů si─Ö w g┼é├│wnej tre┼Ťci funkcji quips().

Po ostatnim wywo┼éaniu metody iter.next() generator dobiega ko┼äca, dlatego w polu .done zwracanego obiektu mamy warto┼Ť─ç true. Zako┼äczenie wywo┼éywania funkcji oznacza praktycznie zwr├│cenie warto┼Ťci undefined, st─ůd te┼╝ warto┼Ť─ç ta pojawia si─Ö w polu .value.

Teraz nadszed┼é odpowiedni moment, by powr├│ci─ç do gadaj─ůcego kota i porz─ůdnie poeksperymentowa─ç z kodem. Wstaw na pr├│b─Ö do p─Ötli instrukcj─Ö yield. Co si─Ö stanie?

Za ka┼╝dym razem, gdy generator wykonuje instrukcj─Ö yield jego ramka stosu ÔÇô zmienne lokalne, argumenty, warto┼Ťci tymczasowe oraz informacja na temat aktualnego miejsca wykonania w kodzie generatora ÔÇô zostaje usuni─Öta ze stosu. Obiekt generatora przechowuje jednak odniesienie do ramki (b─ůd┼║ jego kopi─Ö), dzi─Öki czemu kolejne wywo┼éanie metody .next() mo┼╝e j─ů reaktywowa─ç i kontynuowa─ç wykonywanie funkcji.

Warto w tym miejscu zauwa┼╝y─ç, ┼╝e generatory nie s─ů w─ůtkami. W j─Özykach z w─ůtkami r├│wnocze┼Ťnie mo┼╝na wykonywa─ç kilka blok├│w kodu, co zwykle powoduje wyst─ůpienie zjawiska wy┼Ťcigu (ang. race condition) oraz przek┼éada si─Ö na nieliniowe wykonanie kodu i lepsz─ů wydajno┼Ť─ç. Z generatorami jest zupe┼énie inaczej. Generator wykonywany jest w tym samym w─ůtku co kod inicjuj─ůcy wywo┼éanie. Porz─ůdek wykonywania funkcji jest sekwencyjny i deterministyczny, lecz nigdy wsp├│┼ébie┼╝ny. W przeciwie┼ästwie do w─ůtk├│w systemowych generator mo┼╝e by─ç zawieszony jedynie w miejscach oznaczonych w g┼é├│wnej tre┼Ťci funkcji wyra┼╝eniem yield.

W porz─ůdku. Wiemy ju┼╝, czym s─ů generatory. Zobaczyli┼Ťmy, jak generator zostaje uruchomiony, wstrzymany, a nast─Öpnie wznowiony. A teraz czas na kluczowe pytanie: do czego ta dziwaczna cecha generator├│w mog┼éaby si─Ö przyda─ç?

Generatory s─ů iteratorami

Jak powiedzieli┼Ťmy w poprzednim artykule, iteratory w ES6 nie stanowi─ů pojedynczej wbudowanej klasy, tylko s─ů punktem rozszerzenia j─Özyka. Utworzenie w┼éasnych iterator├│w mo┼╝liwe jest poprzez zaimplementowanie dw├│ch metod: [Symbol.iterator]() i .next().

Implementacja interfejsu zawsze jednak oznacza troch─Ö pracy. Zobaczmy wi─Öc na przyk┼éadzie jak wygl─ůda implementowanie iteratora w praktyce. Utworzymy prosty iterator o nazwie range odliczaj─ůcy od jednej liczby do drugiej, tak jak klasyczna p─Ötla for (;;) z j─Özyka C.

// kod powinien dać sygnał trzy razy
for (var value of range(0, 3)) {
  alert("Ding! Pi─Ötro nr" + value);
}
[/js]

Oto przyk┼éadowe rozwi─ůzanie wykorzystuj─ůce klas─Ö z ES6. (Je┼Ťli sk┼éadnia klas jest dla ciebie niejasna, nie martw si─Ö ÔÇô powiemy o niej w jednej z kolejnych artyku┼é├│w).

[js] class RangeIterator { constructor(start, stop) { this.value = start; this.stop = stop; } [Symbol.iterator]() { return this; } next() { var value = this.value; if (value < this.stop) { this.value++; return {done: false, value: value}; } else { return {done: true, value: undefined}; } } } // zwr├│─ç nowy iterator licz─ůcy od liczby 'start' do 'stop'. function range(start, stop) { return new RangeIterator(start, stop); }

Zobacz ten kod w akcji:

See the Pen JYoKVY by Łukasz Piwko (@shebangpl) on CodePen.

Tak wi─Öc wygl─ůda implementacja iterator├│w w Javie czy Swifcie. Nie najgorzej, ale banalne to nie jest. Czy kod zawiera jakie┼Ť b┼é─Ödy? Trudno powiedzie─ç. Powy┼╝szy kod, maj─ůcy na┼Ťladowa─ç oryginaln─ů p─Ötl─Ö for (;;), zupe┼énie nie przypomina swojego pierwowzoru: protok├│┼é iteratora zmusza nas do zako┼äczenia p─Ötli.

Teraz pewnie tw├│j entuzjazm wobec iterator├│w troch─Ö opad┼é. Mo┼╝na je ┼Ťwietnie wykorzysta─ç, ale ich implementacja wydaje si─Ö trudna.

Pewnie nawet przez my┼Ťl ci nie przesz┼éo, ┼╝e mogliby┼Ťmy wprowadzi─ç do JS dziwaczn─ů i trudn─ů do zrozumienia struktur─Ö sterowania przep┼éywem tylko po to, by u┼éatwi─ç tworzenie iterator├│w. Ale skoro mamy ju┼╝ do dyspozycji generatory, mo┼╝e da┼éoby si─Ö je tutaj wykorzysta─ç? Sprawd┼║my:

function* range(start, stop) {
  for (var i = start; i < stop; i++)
    yield i;
}

Zobacz powy┼╝szy kod w akcji:

See the Pen zvxBQO by Łukasz Piwko (@shebangpl) on CodePen.

Powy┼╝szym 4-linijkowym generatorem mo┼╝na zast─ůpi─ç poprzednie 23 linijki funkcji range(), ┼é─ůcznie z ca┼é─ů klas─ů RangeIterator. Taka zamiana jest mo┼╝liwa, poniewa┼╝ generatory s─ů iteratorami. Wszystkie generatory maj─ů wbudowan─ů implementacj─Ö metod .next() i [Symbol.iterator]() . Programista musi tylko napisa─ç kod realizuj─ůcy dzia┼éanie p─Ötli.

Implementowanie iterator├│w bez generator├│w mo┼╝na por├│wna─ç do sytuacji, w kt├│rej pisz─ůc e-mail dozwolone by┼éoby jedynie korzystanie z konstrukcji w stronie biernej. Kiedy nie mo┼╝emy w prostych s┼éowach wyrazi─ç tego, co mamy na my┼Ťli, tworzymy wypowiedzi, kt├│re s─ů dosy─ç zawi┼ée. Klasa RangeIterator jest d┼éuga i dziwna, poniewa┼╝ musi opisa─ç funkcjonowanie p─Ötli bez korzystania ze sk┼éadni p─Ötli. Rozwi─ůzaniem s─ů generatory.

Jakie jeszcze inne zastosowania mog─ů mie─ç generatory pe┼éni─ůce funkcje iterator├│w?

  • Przekszta┼écanie dowolnego obiektu na obiekt iterowalny. W tym celu wystarczy napisa─ç generator, kt├│ry przegl─ůda obiekt this i na bie┼╝─ůco zwraca odczytan─ů warto┼Ť─ç, a nast─Öpnie zainstalowa─ç go jako metod─Ö [Symbol.iterator] danego obiektu.
  • Upraszczanie funkcji buduj─ůcych tablice. Za┼é├│┼╝my, ┼╝e mamy funkcj─Ö zwracaj─ůc─ů tablic─Ö z wynikami po ka┼╝dym wywo┼éaniu, np.:
    // Dzieli jednowymiarow─ů tablic─Ö icons
    // na tablice o d┼éugo┼Ťci r├│wnej rowLength.
    function splitIntoRows(icons, rowLength) {
      var rows = [];
      for (var i = 0; i < icons.length; i += rowLength) {
        rows.push(icons.slice(i, i + rowLength));
      }
      return rows;
    }

    Dzi─Öki generatorom taki kod mo┼╝na nieco skr├│ci─ç:

    function* splitIntoRows(icons, rowLength) {
      for (var i = 0; i < icons.length; i += rowLength) {
        yield icons.slice(i, i + rowLength);
      }
    }

    Jedyna r├│┼╝nica w zachowaniu generatora polega na tym, ┼╝e zamiast oblicza─ç wszystkie wyniki na raz i zwraca─ç je w formie tablicy, kod zwraca iterator, a ka┼╝dy wynik obliczany jest pojedynczo w razie potrzeby.

  • Wyniki niestandardowych rozmiar├│w. Nie da si─Ö zbudowa─ç niesko┼äczenie d┼éugiej tablicy. Mo┼╝na natomiast zwr├│ci─ç generator, kt├│ry wygeneruje niesko┼äczon─ů sekwencj─Ö, a ka┼╝dy kod inicjuj─ůcy wywo┼éanie b─Ödzie m├│g┼é czerpa─ç z niej tyle warto┼Ťci, ile b─Ödzie wymagane.
  • Refaktoryzacja z┼éo┼╝onych p─Ötli. Tw├│j kod zawiera d┼éug─ů, paskudn─ů funkcj─Ö? Chcia┼éby┼Ť rozbi─ç j─ů na dwie mniej skomplikowane cz─Ö┼Ťci? Generatory to nowe narz─Ödzie, kt├│re mo┼╝esz od dzi┼Ť wykorzystywa─ç do refaktoryzacji kodu. W przypadku skomplikowanych p─Ötli mo┼╝liwe jest wyselekcjonowanie cz─Ö┼Ťci kodu wytwarzaj─ůcej dane i przekszta┼écenie jej na osoby generator. Nast─Öpnie nale┼╝y przedefiniowa─ç p─Ötl─Ö do postaci for (var data of myNewGenerator(args)).
  • Narz─Ödzia do pracy z obiektami iterowalnymi. ES6 nie zawiera rozbudowanej biblioteki narz─Ödzi do filtrowania, mapowania i, og├│lnie rzecz ujmuj─ůc, hakowania iterowalnych zbior├│w danych. Generatory jednak ┼Ťwietnie si─Ö nadaj─ů do tworzenia potrzebnych narz─Ödzi, i to za pomoc─ů zaledwie kilku linijek kodu.

    Załóżmy przykładowo, że potrzebujemy ekwiwalentu metody Array.prototype.filter, który będzie operował nie tylko na tablicach, lecz także na obiektach NodeList ze struktury DOM. Bułka z masłem:

    function* filter(test, iterable) {
      for (var item of iterable) {
        if (test(item))
          yield item;
      }
    }

Czy mo┼╝na zatem powiedzie─ç, ┼╝e generatory s─ů przydatne? Bez w─ůtpienia. Mo┼╝na za ich pomoc─ů w zdumiewaj─ůco ┼éatwy spos├│b zaimplementowa─ç w┼éasne iteratory, kt├│re stanowi─ů nowy standard danych i p─Ötli w ES6.

Nie jest to jednak wszystko, co potrafi─ů generatory. By─ç mo┼╝e nie jest to nawet ich najwa┼╝niejsza w┼éa┼Ťciwo┼Ť─ç.

Generatory i kod asynchroniczny

Oto fragment kodu, kt├│ry napisa┼éem jaki┼Ť czas temu:


          };
        })
      });
    });
  });
});

Mo┼╝e widzia┼ée┼Ť ju┼╝ co┼Ť podobnego we w┼éasnym kodzie. Asynchroniczne interfejsy API zwykle wymagaj─ů wywo┼éania zwrotnego, co za ka┼╝dym razem, gdy chcemy co┼Ť wykona─ç, poci─ůga za sob─ů konieczno┼Ť─ç napisana dodatkowej funkcji anonimowej. Je┼Ťli zatem we┼║miemy kod, kt├│ry nie jest z┼éo┼╝ony z trzech linijek, lecz wykonuje trzy r├│┼╝ne operacje, widzimy w├│wczas trzy poziomy wci─Öcia kodu.

Oto jeszcze jeden fragment napisanego przeze mnie kodu JS:

}).on('close', function () {
  done(undefined, undefined);
}).on('error', function (error) {
  done(error);
});

W asynchronicznych interfejsach API zamiast wyj─ůtk├│w wykorzystuje si─Ö konwencjonalne techniki obs┼éugi b┼é─Öd├│w. R├│┼╝ni─ů si─Ö one mi─Ödzy sob─ů zale┼╝nie od danego API. W wi─Ökszo┼Ťci interfejs├│w b┼é─Ödy s─ů po cichu ignorowane. W niekt├│rych przypadkach domy┼Ťlnie ignorowane jest nawet prawid┼éowe wykonanie kodu.

Do tej pory tego typu problemy by┼éy cen─ů za mo┼╝liwo┼Ť─ç korzystania z kodu asynchronicznego. Zd─ů┼╝yli┼Ťmy si─Ö ju┼╝ pogodzi─ç z tym, ┼╝e kod asynchroniczny nie jest tak prosty i przyjemny dla oka co kod synchroniczny.

Generatory daj─ů jednak nadziej─Ö, ┼╝e wcale nie musi tak by─ç.

Funkcja Q.async() to eksperymentalna pr├│ba wykorzystania generator├│w w po┼é─ůczeniu z obietnicami (ang. promises) w celu napisania kodu asynchronicznego, kt├│ry b─Ödzie odwzorowywa┼é odpowiadaj─ůcy mu kod synchroniczny. Przyk┼éad:

// kod synchroniczny, który trochę pohałasuje
function makeNoise() {
  shake();
  rattle();
  roll();
}

// kod asynchroniczny, który trochę pohałasuje
// zwraca obiekt obietnicy, która zostaje spełniona
// kiedy skończymy już hałasować
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

G┼é├│wna r├│┼╝nica w por├│wnaniu z rozwi─ůzaniem synchronicznym polega na tym, ┼╝e do kodu w wersji asynchronicznej nale┼╝y doda─ç s┼éowo kluczowe yield w ka┼╝dym miejscu, w kt├│rym wywo┼éywana jest funkcja asynchroniczna.

Dodanie teoretycznie ÔÇ×nieproszonejÔÇŁ instrukcji if czy bloku try-catch do wersji Q.async dzia┼éa tak samo, jak dodanie ich do zwyk┼éego kodu synchronicznego. W por├│wnaniu z innymi sposobami, korzystaj─ůc z opisanego rozwi─ůzania nie odnosi si─Ö wra┼╝enia, ┼╝e do napisania kodu asynchronicznego trzeba uczy─ç si─Ö nowego j─Özyka.

Je┼Ťli dotar┼ée┼Ť ju┼╝ do tego miejsca, by─ç mo┼╝e zainteresuje ci─Ö wyczerpuj─ůcy artyku┼é na ten temat autorstwa Jamesa Longa.

Generatory wytyczaj─ů zatem nowy model programowania asynchronicznego, kt├│ry jest o wiele bardziej przyjazny ludzkim umys┼éom. Prace w tym zakresie wci─ů┼╝ trwaj─ů. Jednym z wartych rozwijania udogodnie┼ä by┼éaby przejrzystsza sk┼éadnia. Propozycja funkcji asynchronicznych, opieraj─ůcych si─Ö na obietnicach i generatorach, zainspirowanych analogicznymi rozwi─ůzaniami w j─Özyku C#, b─Ödzie rozwa┼╝ana w pracach nad ES7.

Do czego mog─Ö wykorzysta─ç te szalone dodatki?

Ju┼╝ teraz mo┼╝esz u┼╝ywa─ç generator├│w ES6, pisz─ůc serwerowy kod dla ┼Ťrodowiska io.js (a tak┼╝e Node po zdefiniowaniu opcji --harmony w wierszu polece┼ä).

Je┼Ťli chodzi o kod przegl─ůdarkowy, generatory ES6 s─ů p├│ki co obs┼éugiwane jedynie w przegl─ůdarkach Firefox 27 i Chrome 39 oraz ich nowszych wersjach. Aby m├│c ju┼╝ dzi┼Ť u┼╝ywa─ç generator├│w w kodzie przegl─ůdarkowym, trzeba skorzysta─ç z kompilatora Babel lub Traceur, kt├│ry przet┼éumaczy kod w standardzie ES6 na przyjazny przegl─ůdarkom ES5.

A teraz kilka s┼é├│w uznania dla zas┼éu┼╝onych os├│b: generatory zosta┼éy po raz pierwsze zaimplementowane w JS przez Brendana Eicha. Jego implementacja by┼éa w du┼╝ej mierze oparta na generatorach z Pythona, kt├│re by┼éy z kolei zainspirowane j─Özykiem Icon. Pierwsza wersja generator├│w pojawi┼éa si─Ö oficjalnie w Firefoksie 2.0 w 2006 roku. Droga do ich standaryzacji by┼éa wyboista, a ich sk┼éadnia i zachowanie zmienia┼éy si─Ö kilkakrotnie. Generatory ES6 zosta┼éy zaimplementowane w przegl─ůdarkach Firefox i Chrome przez hakera AndyÔÇÖego Wingo, a ca┼é─ů prac─Ö sponsorowa┼é Bloomberg.

yield;

To nie wszystko je┼Ťli chodzi o generatory. Nie powiedzieli┼Ťmy nic o metodach .throw() i .return(), opcjonalnym argumencie metody .next(), ani o sk┼éadni wyra┼╝enia yield*. S─ůdz─Ö jednak, ┼╝e ten artyku┼é jest ju┼╝ wystarczaj─ůco d┼éugi i zd─ů┼╝y┼é porz─ůdnie namiesza─ç ci w g┼éowie. Tak jak generatory, powinni┼Ťmy si─Ö na razie wstrzyma─ç i wr├│ci─ç do tego tematu za jaki┼Ť czas.

Om├│wili┼Ťmy dwa z┼éo┼╝one zagadnienia z rz─Ödu, wi─Öc w kolejnym artykule czas da─ç sobie nieco luzu. Czy nie by┼éoby mi┼éo powiedzie─ç o jakim┼Ť elemencie ES6, kt├│ry nie zmieni ┼╝ycia programist├│w JS? O czym┼Ť prostym, ┼éatwym i przydatnym? O czym┼Ť, co wywo┼éa na twojej twarzy u┼Ťmiech? W ES6 znajduj─ů si─Ö i takie elementy.

W nast─Öpnym odcinku: co┼Ť, co od razu wkomponujesz w pisany na co dzie┼ä kod. Odwiedzaj nas zatem cz─Östo, by dowiedzie─ç si─Ö, jak funkcjonuj─ů ┼éa┼äcuchy szablonowe w ES6.

Autor: Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/05/es6-in-depth-generators/

Tłumaczenie: Joanna Liana

Tre┼Ť─ç tej strony jest dost─Öpna na zasadach licencji CC BY-SA 3.0

1 komentarz do “Generatory”

Mo┼╝liwo┼Ť─ç komentowania zosta┼éa wy┼é─ůczona.