Computer Keyboard - Głównie JavaScript

Głównie JavaScript

by Jakub T. Jankiewicz

Generatory i Iteratory wyższego poziomu

Microprocesor by Jakub Jankiewicz
Jakub Jankiewicz, żródło Flickr, licencja CC BY-SA

Ten wpis został zainspirowany filmikiem na YouTube na kanale FunFunFunction. @mpj omawia tylko iterator map, ja poszedłem o krok dalej i opisałem trójce funkcjonalnego programowania w JS czyli map, reduce oraz filter.

  • Iterator map

Implementacja iteratora map, a właściwie generatora, wygląda podobnie jak normalnej funkcji map, przy czym zamiast tworzyć nową tablicę i dodając element, który zwróci funkcja, do nowej tablicy używamy słowa kluczowego yield.

const map = function*(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}
  • Iterator filter
const filter = function*(iterable, fn) {
  for (const item of iterable) {
    if (fn(item)) {
      yield item;
    }
  }
}

Poniżej przykład użycia map oraz filter:

// najpierw tworzymy wejściowy iterator range (jak funkcja z Pythona):
function* range(n) {
  let i = 0;
  while(i++ < n) {
    yield i;
  }
}
// funkcje strzałkowe
const even = n => n % 2 == 0;
const square = n => n * 2;
const squares = map(filter(range(10), even), square);
for (const n of squares) {
  console.log(n);
}

Wynikiem będzie

4 8 12 16 20
  • Iterator reduce

Reduce jest trochę bardziej skomplikowany, ponieważ musimy użyć protokołu iteratorów, poniżej funkcja reduce:

const reduce = function*(iterable, fn, init) {
  const iterator = iterable[Symbol.iterator]();
  let acc = arguments.length == 3 ? init : iterator.next().value;
  while (true) {
    const value = iterator.next();
    if (value.done) {
      break;
    }
    acc = fn(acc, value.value);
    // we yield intermediate values but real reduce should not be generators
    // and it should just return acc at the end
    yield acc;
  }
}

Tak naprawdę reduce to nie powinien być generator tylko funkcja zwracająca pojedynczą wartość acc na końcu albo generator z jednym yield też na końcu, za pętlą while. Ale dla testów napisana została jako generator aby można było obserwować wyniki poszczególnych iteracji:

Poniżej przykład użycia generatora reduce, który tworzy silnie od 1 do 10.

const inc = n => n+1;
const mul = (a,b) => a*b;
for (const factorial of reduce(map(range(10), inc), mul, 1)) {
  console.log(factorial);
}

Tutaj link do demka na CodePen z powyższymi przykładami.

Co ciekawe ponieważ tablice także są iteratorami (implementują protokół iteratorów, więcej informacji na stronie Generatory i Iteratory) można ich używać z powyższymi funkcjami.

for (var i of map([1,2,3,4], (x) => x*2)) {
    console.log(i);
}
// aby uzyskać tablice z iteratora można użyć operatora spread albo Array.from
[...map(range(10), square)];

Tak samo jak w przypadku zwykłych iteratorów, możemy napisać iteratory map, reduce oraz filter dla iteratorów asynchronicznych.

  • Asynchroniczny iterator map:
const map = async function*(iterator, fn) {
  for await (const item of iterator) {
    yield fn(item);
  }
}
  • Asynchroniczny iterator filter:
const filter = async function*(iterator, fn) {
  for await (const item of iterator) {
    if (fn(item)) {
      yield item;
    }
  }
}
  • Asynchroniczna funkcja reduce

Tym razem poprawna funkcja reduce

const reduce = async function(iterable, fn, init) {
    // aby funkcja była uniwersjalna można sprawdzać oba symbole
    const iterator = iterable[Symbol.asyncIterator]();
    let acc = arguments.length == 3 ? init : iterator.next().value;
    while (true) {
        const value = await iterator.next();
        if (value.done) {
            break;
        }
        acc = fn(acc, value.value);
    }
    return acc;
}

Przykład użycia

async function* requests(urls) {
    for (const url of urls) {
        yield fetch(url);
    }
}
function terminals(urls) {
    var reqs = requests(urls);
    var texts = map(reqs, res => res.text());
    var titles = map(texts, text => text.match(/<title>([^<]+)<\/title>/)[1]);
    return filter(titles, title => title.match(/terminal/i));
}
function concat_first(acc, title) {
    return (acc ? acc + ' ' : '') + title.split(' ')[0];
}
(async () => {
    var urls = [
        'https://jcubic.pl',
        'https://terminal.jcubic.pl',
        'https://jcubic.github.io/git/'
    ];
    for await (const title of terminals(urls)) {
        console.log(title);
    }
    console.log(await reduce(terminals(urls), concat_first, ''));
})();

Aby można było skorzystać z iteratora dwa razy, zapisano nasze transformacje wewnątrz funkcji terminals.

Na koniec demko do części asynchronicznej, tym razem na jsBin ponieważ parser JavaScript, który jest na CodePen, nie lubi asynchronicznych generatorów :(

Ostatnio na JavaScript Weekly (newsletterze dotyczącym języka JavaScript, który polecam), pojawił się link do biblioteki axax, która udostępnia m.i. funkcja omawiane w tym wpisie, które działają dla obu typów iteratorów.

źródło strony (aby zobaczyć kod na GitHubie, musisz kliknąć przycisk raw)
Komentarze

Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.