
Генератор - это функция, которая при каждом вызове возвращает следующее значение из последовательности.
Сочетание функциональных генераторов с оператором конвейера и чистых функций с намерением раскрытия имен позволяет писать код более выразительно, без создания промежуточных списков:
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.
