ES6 – generators

javascript

Generator to chyba najbardziej tajemnicza funkcjonalność, pojawiająca się wraz ze standardem ECMAScript2015. Jest to pewien podtyp iteratora, o którym pisałem poprzednio, a zarazem specjalny rodzaj funkcji, której wykonanie może zostać wstrzymane, a potem znów wznowione.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Na początek chciałbym wprowadzić dwa nowe słowa kluczowe języka JavaScript:

yield – operator zwraca wartość z funkcji w momencie, gdy następuje zatrzymanie działania iteratora (wykonanie metody next()).

funtion* – specjalny rodzaj funkcji, która zwraca instancję generatora.

Spójrzmy na przykład prostego generatora:

function* generator () {
  yield 1; 
  // pause
  yield 2; 
  // pause
  yield 3; 
  // pause
  yield 'done?'; 
  // done
}

let gen = generator(); // [object Generator]
console.log(gen.next()); // Object {value: 1, done: false}
console.log(gen.next()); // Object {value: 2, done: false}
console.log(gen.next()); // Object {value: 3, done: false}
console.log(gen.next()); // Object {value: 'done?', done: false}

console.log(gen.next()); // Object {value: undefined, done: true}
console.log(gen.next()); // Object {value: undefined, done: true}

for (let val of generator()) {
  console.log(val); // 1
                    // 2
                    // 3
                    // 'done?'
}

Widzimy, że generator tworzony jest z wykorzystaniem function*, po którym standardowo podajemy nazwę funkcji. W środku znajdujemy cztery operatory yield, zwracające kolejne wartości: 1, 2, 3, ‘done?’. Następnie przypisujemy instancję generatora do let get poprzez standardowe wywołanie funkcji. Potem wywołujemy metodę next(), która pozwala pobrać kolejną wartość zwracaną przez operator yield oraz zatrzymuje działanie generatora. Metoda next() jest w zasadzie odpowiednikiem znanym już z iteratorów. Po czwartym wykonaniu metody next(), nie posiadamy już kolejnych operatorów yield, zwracających wartość, więc kolejne wywołanie zwraca nam obiekt:

{
  value: undefined,
  done: true
}

Tym samym własność done informuje nas, że generator zakończył swoje działanie. Dzięki temu, że mamy do dyspozycji metodę next(), a sam generator jest podtypem iteratora, bez przeszkód możemy go wykorzystać w pętli for-of, co pokazane zostało na końcu przykładu.

Poniżej przedstawiam kolejny generator, który zwraca losowe liczby od 1 do 10 i może je zwracać bez końca, zawsze wtedy gdy wywołamy metodę next():

function* random1_10 () {
  while (true) {
    yield Math.floor(Math.random() * 10) + 1;
  }
}

let rand = random1_10();
console.log(rand.next());
console.log(rand.next());

// …

W ES5 byłoby to coś w rodzaju:

function random1_10 () {
  return {
    next: function() {
      return {
        value: Math.floor(Math.random() * 10) + 1,
        done: false
      };
    }
  };
}

let rand = random1_10();
console.log(rand.next());
console.log(rand.next());

// …

Oczywiście nic nie stoi na przeszkodzie, by w generatorach używać innych generatorów i mieszać je razem ze sobą. Ilustruje to poniższy przykład, który jest małym rozwinięciem powyższego. Tym razem zwracane będą wartości od 1 do 20:

function* random (max) {
  yield Math.floor(Math.random() * max) + 1;
}

function* random1_20 () {
  while (true) {
    yield* random(20);
  }
}

let rand = random1_20();
console.log(rand.next());
console.log(rand.next());

// …

Generatory w PHP

generator

Wraz z pojawieniem się PHP 5.5 możliwe jest w końcu tworzenie tak zwanych iteratorów, które nie wymagają specjalnych interfejsów oraz nadmiaru kodu do poprawnego działania. Z pewnością jest to jedna z ciekawszych nowości, którą wprowadza nowa wersja PHP. W dzisiejszym wpisie przedstawię Wam kilka przykładów użycia.

Czym właściwie jest generator?

Definicja, którą znajdziemy na Wikipedii:

Generator jest bardzo podobny do funkcji zwracającej tablicę w tym, że tak jak ona może być wywoływany z argumentami i generuje listę wartości. Jednak zamiast budować pełną tablicę zawierającą wszystkie elementy i zwracać je wszystkie na raz, generator zwraca je po jednym, co znacznie oszczędza pamięć i pozwala funkcji wywołującej generator korzystać z danych od razu już od pierwszych elementów.

Dostępne do tej pory w takich językach jak: Java, C#, Ruby, Python i JavaScript, generatory są czymś co wygląda jak funkcja, ale działa jak iterator. Tym samym generator nie zwraca wartości poprzez słowo kluczowe return, a yield. Oba mają bardzo podobne działanie, ale w przypadku użycia yield zamiast usuwać funkcję ze stosu, to jej stan jest zapisywany. Pozwala to wznowić działanie funkcji podczas jej następnego użycia i zacząć od miejsca, w którym skończyła ona swoje działanie poprzednio. Co więcej – nie jesteśmy w stanie zwrócić wartości z funkcji za pomocą słowa kluczowego return, ale możemy dzięki niemu zakończyć jej działanie i usunąć ją ze stosu.

Pierwszy generator

Wywołanie poniższego kodu:

<?php

function generator1() {
    echo "Start!\\n"; 
    for ($i = 1; $i <= 5; ++$i) {
        yield $i;
        echo "$i\\n";
    }
    echo "Koniec :(\\n"; 
}

foreach (generator1() as $wartosc);

pozwoli na otrzymanie następującego wyniku:

Start!
1
2
3
4
5
Koniec :(

Klucz => wartość

W powyższym przykładzie zostały zwrócone jedynie wartości zmiennej $i, gdzie klucze domyślnie są wartościami liczbowymi. W iteratorach zwracane mogę być też tablice asocjacyjne w postaci klucz => wartość.

<?php

function klucz_wartosc($filename) {
    ...
        yield $key => $line; 
    ...
}

foreach (klucz_wartosc('somefile') as $key => $line) {
    // TODO
}

Przekazywanie (wstrzykiwanie) wartości

yield pozwala nam nie tylko zwracać wartości, a także na ich odbieranie z zewnątrz. Możemy tego dokonać poprzez wywołanie metody send() na obiekcie generatora. Wykonanie tej metody pozwala przesłać do generatora wartości, które użyte mogą zostać podczas pracy samego generatora.

<?php

function wstrzykiwanie() {
    for ($i = 1; $i < 5; ++$i) {
        // pobieramy wartosc z wywolania send()
        $cmd = (yield $i);
        if ($cmd == 'stop') {
            return; // konczymy generator
        }
    }
}
 
$gen = wstrzykiwanie();
 
foreach ($gen as $v) {
    // konczymy
    if ($v == 3) {
        $gen->send('stop');
    }
    echo "{$v}\\n";
}

i w ten sposób otrzymamy:

1
2
3

Podsumowanie

Generatory pozwalają także zaoszczędzić sporo pamięci podczas wykonywania naszego kodu PHP. Wyobraźmy sobie, że mamy do wygenerowania / przetworzenia jakiś duży zbiór danych, gdzie nie jesteśmy pewni, że zawsze potrzebne będą nam wszystkie dane. Z pomocą generatora nasz proces zajmie w takim wypadku znacznie mniej pamięci, a samo wykonanie będzie po prostu szybsze.

Dzięki generatorom programiści otrzymują kolejne warte użycia narzędzie, które nie tylko pozwoli przyspieszyć niektóre kawałki kodu, ale sprawi, że będą mogli swój kod napisać dużo szybciej i bardziej czytelnie.