Введение в функциональное программирование на JavaScript

5

Как обновлять объекты в immutable стиле

Посмотрим сначала неправильный вариант.

const meal = {
    id: 1,
    description: 'Breakfast'
};

const updateMeal = {
    id: meal.id,
    description: meal.description,
    calories: 600
};

Так всё будет работать, однако представьте объект с 10ю параметрами. Вы задолбаетесь заполнять их руками.

Нам на помощь придёт spread оператор. Вместо того, чтобы держать свойства id и description, просто укажем ...meal. Когда мы используем оператор spread, что же происходит? Объект meal вхерячивается в наш новый объект.

const meal1 = {
    id: 1,
    description: 'Breakfast'
};

const updateMeal1 = {
    ...meal1,
    calories: 600
};

console.log(meal1);
console.log(updateMeal1);

Также стоит обратить внимание, что если мы вставим после спред операции свойство, которое есть в спреде, то оно просто его перезапишет.

const meal1 = {
    id: 1,
    description: 'Breakfast'
};

const updateMeal1 = {
    ...meal1,
    description: 'Egg',
    calories: 600
};

console.log(meal1);
console.log(updateMeal1);

В этом случае в объекте updateMeal1 в описании будет яйцо — Egg.

А что если мы захотим удалить что нибудь в immutable стиле? Для этого мы воспользуемя объединением двух операторов: destructuring и rest.

Пример destructuring

const { description, calories } = updateMeal1;

Вот как мы удалим id.

const { id, ... mealWithoutId} = updateMeal1;

При таком раскладе мы выкидываем id в левую константу, а в объект mealWithoutId попадает весь остальной объект.

const updatedMeal = {
  ...meal,
  calories: 200
}

console.log(updatedMeal);

const updatedMealNew = {
  ...updatedMeal,
  calories: updatedMeal.calories + 100
}

console.log(updatedMealNew);

const {calories, ...updatedMealWithoutId} = updatedMealNew;
console.log(updatedMealWithoutId);

Как обновить массивы immutable способом

Самый простой способ обновить массив не изменяя старого, опять воспользовавшись спред оператором.

const meals = [
    {id: 1, description: 'Breakfast', calories: 420},
    {id: 2, description: 'Lunch', calories: 520},
];

const meal = [
    {id:3, description: 'Snack', calories: 180}
];

const updatedMeals = [...meals, meal];
console.log(meals, updatedMeal);

Но что, если мы захотим что либо изменить в нашем массиве. Возможно Вы слышали про функцию map.
Например, мы хотим изменить описание в объект с id 2.

const updatedMealsDescription = updateMeals.map(updateDescription);

function updateDescription(meal) {
    if(meal.id === 2) {
        return {
            ...meal,
            description: 'Early Lunch'
        };
    }
    return meal;
}

Теперь посмотрим, как нам удалить значение из массива immutable способом.
Для этого воспользуеся методом filter. Допустим удалим элемент массива с id не равным 1.

const filteredMeals = updatedMeal.filter(meal => meal.id !== 1);

Теперь рассмотрим следующий небольшой Пример

Как получить итоговое значение элементов массива.

Есть такой мощный метода массива как reduce
Например есть у нас массив оценок и нам нужно получить среднюю оценку, а также какое количество оценок есть.

const grades = [60, 55, 80, 90, 99, 92, 75, 72];
const total = grades.reduce(sum);

function sum (acc, grade) {
    return acc + grade;
}

const count = grades.length;

const letterGradeCount = grades.reduce(groupByGrade, {});

function groupByGrade(acc, grade) {
    const {
        a = 0,
        b = 0,
        c = 0,
        d = 0,
        f = 0
    } = acc;
    if (grade >= 90) return {...acc, a: a + 1};
    if (grade >= 80) return {...acc, b: b + 1};
    if (grade >= 70) return {...acc, c: c + 1};
    if (grade >= 60) return {...acc, d: d + 1};
    if (grade >= 50) return {...acc, f: f + 1};
}

console.log(total, total / count, letterGradeCount);

Или еще один Пример

const reviews = [4.5, 4.0, 5.0, 2.0, 1.0, 5.0, 3.0, 4.0, 1.0, 5.0, 4.5, 3.0, 2.5, 2.0];

// 1. Using the reduce function, create an object that
// has properties for each review value, where the value
// of the property is the number of reviews with that score.
// for example, the answer should be shaped like this:
// { 4.5: 1, 4.0: 2 ...}

const reviewsCount = reviews.reduce((acc, point) => {
  if(!acc[point]) acc[point] = 0;
  return {...acc, [point]: acc[point] + 1};
}, {});

console.log(reviewsCount);

// ====================================

const countGroupedByReview = reviews.reduce(groupBy, {});

function groupBy (acc, review){
  const count = acc[review] || 0;
  return {...acc, [review]: count + 1}
}

Каррирование

Вот есть у нас вот такой вот код

function greet (greeting, name) {
    return `${greeting} ${name}`;
}

console.log(greet('Good morning', 'Eugene'));

const friends = ['Eugene', 'Alex', 'Dima', 'Ura'];

Очевидно, что функция принимает два аргумента — приветствие и имя. И просто выводит их в консоль. Всё очень просто. Но что если мы хотим вывести приветствие для целого списка имён?

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

Как бы нам быстренько переделать массив имён, в массив приветствий?

Можно воспользоваться функцией map. Но ведь она принимает только один аргумент для обработки, второй является порядковым числом элемента!
Как мы можем передать в map функцию с двумя параметрами?

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

function greet(greeting) {
    return function (name) {
        return `${greeting} ${name}`;
    }
}

console.log(greet('Good morning')('Eugene'));

const friends = ['Eugene', 'Alex', 'Dima', 'Ura'];

const friendGreetings = friends.map(greet('Good morning'));
console.log(friendGreetings);
```function greet (greeting, name) {
    return `${greeting} ${name}`;
}

console.log(greet('Good morning', 'Eugene'));

const friends = ['Eugene', 'Alex', 'Dima', 'Ura'];

const friendGreetings = friends.map(greet.bind(null, 'Good morning'));
console.log(friendGreetings);

Или можно сделать так с аналогичным результатом

``` javascript
function greet (greeting, name) {
    return `${greeting} ${name}`;
}

console.log(greet('Good morning', 'Eugene'));

const friends = ['Eugene', 'Alex', 'Dima', 'Ura'];

const friendGreetings = friends.map(greet.bind(null, 'Good morning'));
console.log(friendGreetings);

Что такое функции высшего порядка (high-order function)?
Это функции, которые принимают другую функцию в качестве аргумента, или возвращают функцию.

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

Частичное применение (Partial application)

Есть принцип, близкий к каррированию, называемый частичное применение.

function greet(greeting) {
    return function (name) {
        return `${greeting} ${name}`;
    }
}
const morningGreetingFunction = greet('Good Morning');
console.log(morningGreetingFunction('Nate'));

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

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

// partial: allows for partial application

function add(x,y) {
    return x + y;
}

Итак опять вернемся к рассуждению о каррированной функции и частичном применении. Каррирование относится к создании функции. Когда Вы каррируете, то у Вас отсутствуют данные.

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

Каррирование и частичное применение — это очень важный принцип для функционального программирования. Это одни из тех 20%, которые будут делать 80% кода.

Что будет если нам нужно будет каррировать много функций…
Получится что то типа этого

summary(2)('Hello')(35)(67)(99)('add');

Это не является хорошей практикой. Вообще javascript плевать хотел на каррирование. Просто так уж вышло, что он поддерживает эту возможность. Для более удобной работы в функциональном стиле есть хорошая библиотека — Ramda.

Давайте её и воспользуемся.
Напишем новую функцию с использованием Ramda. В данном примере воспользуемся альтернативным способом создания функций. Fat arrow function или по другому иногда называют лямда синтаксисом.
() => {}
Для того чтобы воспользоваться библиотекой для каррирования, просто обернем нашу функцию в метод curry.

const greet = R.curry((greeting, name) => `${greeting} ${name}`);
console.log(greet('Good Morning')('Eugene'));
const morningGreeting = greet('Top of the morning to ya ');
console.log(morningGreeting('James'));
const friends = ['Nate', 'Jim', 'Scott', 'Dean'];
const friendGreetings = friends.map(greet('Good Morning'));
console.log(friendGreetings);

Pure function vs Impure functions (procedure)

Чистая функция — это функция, которая создает и возвращает значение базирующееся только от входных параметров и не вызывает побочных эффектов (side effect).

Есть несколько правил, которых стоит держаться, чтобы функции были чистыми (pure):
* Во-первых, чистые функции должны иметь входные параметры
* Во-вторых, чистые функции не должны использовать никакие значения состояния (например функция не должна зависеть ни от одной переменной извне их самих, которые могут поменяться со временем).
* Чистая функция должна возвращать значение, которое определяется только с помощью входного параметра
* Чистая функция не должна вызывать никаких побочных эффектов.

Что такое побочный эффект?
Вы можете думать, что побочный эффект, это когда Ваш код вызывает некоторые изменение вне себя. Например, если вы запускаете функцию. После того как функция завершилась, код внутри функции вызвал постоянные изменения. Эти постоянные изменения и есть побочный эффект.

Некоторые примеры побочных эффектов — это такие вещи как сохранение чего либо в базу данных, или запись в файл какие то изменения в браузере.

Давайте взглянем на нечистую функцию (impure function).

let counter = 0;

function increment() {
    counter++;
}

Эта функция нарушает все 4 правила, которые мы рассмотрели Выше. Но достаточно нарушить всего одно правило, для того чтобы функция перестала быть чистой.

Использование чистых функций на порядок сложнее обычных. Так зачем же их использовать?

Вот несколько главных причин:

  • Чистые функции переиспользуемые (reusable)
  • Чистые функции компонуемы (composable). Что означает, что Вы можете объединять их, чтобы эффективно создавать новые функции.
  • Чистые функции легко тестировать
  • Чистые функции всегда выдают одинаковый результат для одних и тех же аргументов.

Терь вопрос, как написать приложение, у которого есть состояния, которые меняются ).

Функциональное программирование не говорит, что не должно быть состояния (state).
Ведь это глупо. ФП о том, чтобы устранять состояние как можно больше и жестко контролируя это состояние.

Function composition

Что такое function composition?
Это создание новых функций из других функций с помощью комбинации логики этих других функций.
Например, у нас есть функция под названием slice, которая берет яблоки и возвращает порезанные яблоки.

function slice() {
    return ;
}

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

function bake () {
    return ;
}

Имейте в виду, что выходные данные функции slice, это ожидаемые входные данные функции bake. Что если мы скормим выходные данные slice прямо во входные bake?

Когда выходные данные одной чистой функции с таким же типом данных, которые ожидаются во входных данных другой чистой функции, вы можете соединить их или как это называется — сделать композицию из них (compose).

Например мы можем сделать новую функцию под названием makePie, которая является составной из bake и slice.

const makePie = compose(bake, slice);
const pie = makePie(apple);

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

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

Давайте посмотрим на пару практических примеров.

const sentence = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Est minima cum aspernatur fugit libero ducimus facilis ea eaque neque eos sapiente dignissimos voluptatum, commodi doloribus deserunt, sed tenetur inventore tempora!"

Допустим есть у нас предложение. И мы хотим узнать сколько слов в нём. Мы опять воспользуемся библиотекой Ramda.

Сначала разобьем предложение на слова.

const wordList = R.split(' ', sentence);
const wordCount = R.length(wordList);

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

const wordCount = R.length(R.split(' ', sentence));

Но это не совсем правильный подход, это становится сложнее читать, особенно если объем растет.
А теперь реализуем решение нашей задачи с помощью Ramda и композиции. Воспользуемся функцией compose. Она работает справа налево, т.е. последним аргументом должная идти первая функция.

const countWords = R.compose(R.length, R.split);
console.log(countWords(' ', sentence));

Мы объеденили логику двух функций и получили третью функцию, которую мы можем переиспользовать где захотим, если понадобится.
А теперь сделаем так, чтобы не надо было каждый раз вводить пробел в качестве первого аргумента. Все методы в ramda каррируются. Поэтому просто передадим split первый параметр.

const countWords = R.compose(R.length, R.split(' '));
console.log(countWords(' ', sentence));

Также у ramda есть метод pipe, который по своей сути тоже самое что compose, только функции обрабатываются слева-направо.

const countWords2 = R.pipe(R.split(' '), R.length);
console.log(countWords2(sentence));

You might also like More from author

Leave A Reply

Your email address will not be published.