Генератор - это функция, которая при каждом вызове возвращает следующее значение из последовательности.

Сочетание функциональных генераторов с оператором конвейера и чистых функций с намерением раскрытия имен позволяет писать код более выразительно, без создания промежуточных списков:

import { sequence, filter, map, take, toList } from "./sequence";
const filteredTodos =
  sequence(todos) 
  |> filter(isPriorityTodo) 
  |> map(toTodoView)
  |> take(10)  
  |> toList;

Посмотрим как.

Я начну с простого функционального генератора, который при каждом вызове возвращает следующее целое число. Начинается с 0.

function sequence() {
  let count = 0;
  return function() {
    const result = count;
    count += 1;
    return result;
  }
}
const nextNumber = sequence();
nextNumber(); //0
nextNumber(); //1
nextNumber(); //2

nextNumber() - бесконечный генератор. nextNumber() также является функцией закрытия.

Конечный генератор

Генераторы могут быть конечными. Посмотрите следующий пример, где sequence() создает генератор, который возвращает последовательные числа из определенного интервала. В конце последовательности он возвращает undefined:

function sequence(from, to){
 let count = from;
 return function(){
   if(count< to){
      const result = count;
      count += 1;
      return result;
    }
  }
}
const nextNumber = sequence(10, 15);
nextNumber(); //10
nextNumber(); //12
nextNumber(); //13
nextNumber(); //14
nextNumber(); //undefined

к списку()

При работе с генераторами мы можем захотеть создать список со всеми значениями из последовательности. В этой ситуации нам нужна новая функция toList(), которая принимает генератор и возвращает все значения из последовательности в виде массива. Последовательность должна быть конечной.

function toList(sequence) {
  const arr = [];
  let value = sequence();
  while (value !== undefined) {
    arr.push(value);
    value = sequence();
  }
  return arr;
}

Давайте использовать его с предыдущим генератором.

const numbers = toList(sequence(10, 15));
//[10,11,12,13,14]

Оператор трубопровода

Конвейер - это серия преобразований данных, в которой выходные данные одного преобразования являются входными данными следующего.

Оператор конвейера |> позволяет нам более выразительно записывать преобразования данных. Оператор конвейера обеспечивает синтаксический сахар над вызовами функций с одним аргументом. Рассмотрим следующий код:

const shortText = shortenText(capitalize("this is a long text"));
function capitalize(text) {
  return text.charAt(0).toUpperCase() + text.slice(1);
}
function shortenText(text) {
  return text.substring(0, 8).trim();
}

С помощью оператора конвейера преобразование можно записать так:

const shortText = "this is a long text" 
  |> capitalize 
  |> shortenText;
  //This is

На данный момент оператор трубопровода находится в стадии эксперимента. Вы можете попробовать это с помощью Babel:

  • в package.json файле добавьте плагин babel pipeline:
{
  "dependencies": {
    "@babel/plugin-syntax-pipeline-operator": "7.2.0"
  }
}
  • в файле конфигурации .babelrc добавить:
{
  "plugins": [["@babel/plugin-proposal-pipeline-operator", {
             "proposal": "minimal" }]]
}

Генераторы над коллекциями

В разделе Сделайте ваш код более удобным для чтения с помощью функционального программирования у меня был пример обработки списка todos. Вот код:

function isPriorityTodo(task) {
  return task.type === "RE" && !task.completed;
}
function toTodoView(task) {
  return Object.freeze({ id: task.id, desc: task.desc });
}
const filteredTodos = todos.filter(isPriorityTodo).map(toTodoView);

В этом примере список todos проходит два преобразования. Сначала создается отфильтрованный список, затем создается второй список с сопоставленными значениями.

С помощью генераторов мы можем выполнить два преобразования и создать только один список. Для этого нам понадобится генератор sequence(), который выдает следующее значение из коллекции.

function sequence(list) {
  let index = 0;
  return function() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      return result;
    }
  };
}

filter () и map ()

Далее нам понадобятся два декоратора filter() и map(), которые работают с функциональными генераторами.

filter() берет генератор и создает новый генератор, который возвращает только значения из последовательности, удовлетворяющей функции предиката.

map() берет генератор и создает новый генератор, который возвращает сопоставленное значение.

Вот реализации:

function filter(predicate) {
  return function(sequence) {
    return function filteredSequence() {
      const value = sequence();
      if (value !== undefined) {
        if (predicate(value)) {
          return value;
        } else {
          return filteredSequence();
        }
      }
    };
  };
}
function map(mapping) {
  return function(sequence) {
    return function() {
      const value = sequence();
      if (value !== undefined) {
        return mapping(value);
      }
    };
  };
}

Я хотел бы использовать эти декораторы с оператором конвейера. Итак, вместо создания filter(sequence, predicate){ } с двумя параметрами я создал его каррированную версию, которая будет использоваться следующим образом: filter(predicate)(sequence). Таким образом, он отлично работает с оператором конвейера.

Теперь, когда у нас есть набор инструментов, состоящий из функций sequence, filter, map и toList, для работы с генераторами над коллекциями, мы можем поместить их все в модуль ("./sequence"). См. Ниже, как переписать предыдущий код с помощью этого набора инструментов и оператора конвейера:

import { sequence, filter, map, take, toList } from "./sequence";
const filteredTodos =
  sequence(todos) 
  |> filter(isPriorityTodo) 
  |> map(toTodoView) 
  |> toList;

Вот тест производительности, измеряющий разницу между использованием методов массива и использованием функциональных генераторов. Кажется, что подход с функциональными генераторами на 15–20% медленнее.

уменьшать()

Возьмем еще один пример, который вычисляет цену на фрукты из списка покупок.

function addPrice(totalPrice, line){
   return totalPrice + (line.units * line.price);
}
function areFruits(line){
   return line.type === "FRT";
}
let fruitsPrice = shoppingList.filter(areFruits).reduce(addPrice,0);

Как видите, он требует, чтобы мы сначала создали отфильтрованный список, а затем вычислили общую сумму в этом списке. Давайте перепишем вычисление с помощью функциональных генераторов и избежим создания отфильтрованного списка.

Нам нужна новая функция в панели инструментов: reduce(). Требуется генератор и сводит последовательность к одному значению.

function reduce(accumulator, startValue) {
  return function(sequence) {
    let result = startValue;
    let value = sequence();
    while (value !== undefined) {
      result = accumulator(result, value);
      value = sequence();
    }
    return result;
  };
}

reduce() имеет немедленное исполнение.

Вот код, переписанный с помощью генераторов:

import { sequence, filter, reduce } from "./sequence";
const fruitsPrice = sequence(shoppingList) 
  |> filter(areFruits) 
  |> reduce(addPrice, 0);

брать()

Другой распространенный сценарий - взять только первые n элементов из последовательности. В этом случае нам нужен новый декоратор take(), который получает генератор и создает новый генератор, который возвращает только первые n элементов из последовательности.

function take(n) {
  return function(sequence) {
    let count = 0;
    return function() {
      if (count < n) {
        count += 1;
        return sequence();
      }
    };
  };
}

Опять же, это каррированная версия take(), которую следует называть так: take(n)(sequence).

Вот как можно использовать take() для бесконечной последовательности чисел:

import { sequence, toList, filter, take } from "./sequence";
function isEven(n) {
  return n % 2 === 0;
}
const first3EvenNumbers = sequence()  
  |> filter(isEven) 
  |> take(3) 
  |> toList;
  //[0, 2, 4]

Я переделал предыдущий тест производительности и использую take() для обработки только первых 100 элементов. Оказывается, версия с функциональными генераторами намного быстрее (примерно в 170 раз быстрее).

let filteredTodos = todos
 .filter(isPriorityTodo)
 .slice(0, 100)
 .map(toTodoView);
//320 ops/sec
let filteredTodos =
const filteredTodos =
  sequence(todos) 
  |> filter(isPriorityTodo) 
  |> map(toTodoView)
  |> take(100)
  |> toList;
//54000 ops/sec

Пользовательские генераторы

Мы можем создать любой собственный генератор и использовать его с набором инструментов и оператором конвейера. Давайте создадим собственный генератор Фибоначчи:

function fibonacciSequence() {
  let a = 0;
  let b = 1;
  return function() {
    const aResult = a;
    a = b;
    b = aResult + b;
    return aResult;
  };
}
const fibonacci = fibonacciSequence();
fibonacci();
fibonacci();
fibonacci();
fibonacci();
fibonacci();
const firstNumbers = fibonacciSequence()  
  |> take(10) 
  |> toList;
  //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Заключение

Оператор конвейера делает преобразование данных более выразительным.

Функциональные генераторы могут быть созданы на основе конечных или бесконечных последовательностей значений.

С помощью генераторов мы можем выполнять обработку списков без создания промежуточных списков на каждом этапе.

Вы можете проверить все образцы на codeandbox.