Разработка на React. Функциональное программирование

Продолжая серию статей о React, поговорим о функциональной парадигме программирования, так как React и Flux основаны на функциональных методах.

Функциональное программирование одна из “горячих” тем из мира JavaScript. Но как раздел дискретной математики и парадигма программирования существует еще с давних пор. Функциональному, как правило, противопоставляется императивный подход к программированию.

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

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

Многие использовали некоторые концепции функционального программирования в императивном подходе сами того не понимая, к примеру, “анонимные функции” или “callback-функции”, которые хорошо развиты в таких языках как C#, Python и др. Также анонимные функции (они же “стрелочные функции” =>) появились в ES6 версии JavaScript, что сделало его более пригодным к функциональному подходу.

История

Истоками функционального программирования является лямбда-исчисление, которое появилось в далеких 1930-х, благодаря математику Алонзо Черчу (Alonzo Church). Которое было разработано для формализации и анализа вычислимости. Это было формальной системой до тех пор, пока в 1950-х Джон Маккарти (John McCarthy), исследователь в области искусственного интеллекта, не проявил интерес к исследованиям Алонзо Черча. В 1958 году Джон Маккарти уже представил язык программирования LISP, основанный на лямбда-логике, он и стал первым функциональным языком. Хотя с первых версий и имел наклонности к императивности.

Концепции

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

Чистые функции

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

Побочными эффектами функции являются:

  • Чтение и запись глобальных переменных;
  • Реагирование и обработка внешних событий;
  • Зависимость результатов от внешних факторов;
  • Выполнение ввода-вывода.

Библиотека контроля состояний - Redux, которая включает в себя ядро с идеей чистых функций.

Состояние

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

// с состоянием
let magicNum = 1;

let changeMagicNumber() =>  {
	magicNum += 10;
}

changeMagicNumber();
console.log(magicNum);
// без состояния
let magicNum = 1;

let changeMagicNumber(val) {
	return val + 10;
}

magicNum = changeMagicNumber(magicNum);
console.log(magicNum);

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

В React существует понятие Stateless Functional Component. Это компоненент который не содержит состояния (state) или ссылок на DOM объекты, в них передаются параметры props и контекст. Повторюсь, такие функции легко переиспользовать и тестировать, так как они не имеют побочных эффектов.

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

Плюсы:

  • Тестируемость
  • Ссылочная прозрачность

Минусы:

  • Нагрузка на память
  • Нагрузка на CPU

Неизменяемость

Неизменяемость подразумевает под собой, что во время выполнения программа не может изменять свои данные, а лишь создавать их копии. Другими словами неизменяемый объект (immutable) нельзя изменить после его создания, и наоборот изменяемый объект (mutable) может быть изменен после создания. В js неизменяемыми являются примитивные типы данных. Примитивный тип данных хранится как значение, а объекты как ссылка. По этому при изменении примитивных типов данных всегда новое значение, без изменения старого. Изменяемые данные хороши тем, что не нужно создавать новых объектов. Но это опасно, так как при изменении этого объекта, все ссылки на него будут содержать те же самые данные.

// изменяемые данные
let array1 = [];
let array2 = array.push('item1');
console.log(array1 === array2) // true
// неизменяемые данные
let val1 = 1;
let val2 = val1 + 1;
console.log(val1 === val2) // false 

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

С выхода ES6 работать с неизменяемыми данными стало гораздо проще. К примеру, новый объект можно создать через Object.assign() или через оператор расширения.

// клонирование объекта через Object.assign
let obj1 = { a: 1 };
let obj2 = Object.assign({}, obj1);
console.log(obj1 === obj2) // false

Но такой синтаксис может ухудшить читаемость кода. Альтернативный вариат – использовать object spread syntax и object spread syntax, он же оператор распространения.

//клонирование массива через spread-оператор
let arr1 = ['data1', 'data2'];
let arr2 = [...arr1];
console.log(arr1 === arr2); // false

//клонирование объекта через spread-оператор
let obj1 = {a: 1};
let obj2 = {...object1};
console.log(obj1 === obj2) // false

Плюсы:

  • Код становится более прозрачным;
  • Меньше неочевидных ошибок;
  • Проще мемоизировать;
  • Атомарность создания объектов;
  • Проще тестировать.

Минусы:

  • Нагрузка на CPU;
  • Нагрузка на память и сборщик мусора.

Функции высших порядков

Функции высших порядков - это такие функции которых один из аргументов имеется функция и/или возвращает функцию. Ярким примером в JavaScript являются встроенные функции map, filter, join и т.д

// пример функции высшего порядка
const addition = (val1, val2) => val1 + val2;
const sum = addition;
console.log(sum(2, 2)) // 4

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

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

// пример каррирования
let property = curry((key, target) => target[key]);
let duration = property('duration');
duration({ duration: 100, ... }); 
property('name', {name: 'Alex'});  

Композиция

Композиция функций (или суперпозиция функций) - применение результата одной функции в качестве аргумента другой.

Например:

fn1(fn2(val))

Так как при функциональном подходе логика программы разбивается на небольшие чистые функции, то для работы приложения необходимо их собрать в более крупные функции. Этот процесс называется композицией. При составлении композиции используется много различных подходов. Один из них, вероятно вам уже знакомый, композиция через вложение функций в аргументы другой функции

// пример суперпозиции функций
unction addOne(x) {
  return x + 1;
}
function timesTwo(x) {
  return x * 2;
}
console.log(addOne(timesTwo(3))); //7
console.log(timesTwo(addOne(3))); //8

Но такой способ имеет существенный недостаток – он плохо масштабируем. Есть более гибкое решение, композиция через compose. У этого метода, также существует множество реализаций. Приведу пример, с реализацией через call.

// пример реализации compose #1
Function.prototype.compose = function(g) {
     let fn = this;
     return function() {
         return fn.call(this, g.apply(this, arguments));
     };
};
let f = mult2.compose(square).compose(add1);
console.log(f(1));

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

// пример реализации compose #2
const compose = (...fns) =>
 (arg) =>
   fns.reduce(
     (composed, f) => f(composed),
     arg
   )
let fn = compose(fn1,fn2, fn3);
fn(1);

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

Рекурсия

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

Классическим примером является вычисление факториала.

Сначала вычислим факториал посредством циклов:

// императивный подход
function factorial(num)
{
    if (num < 0) {
        return -1;
    }
    else if (num == 0) {
        return 1;
    }

    let tmp = num;
    while (num-- > 2) {
        tmp *= num;
    }
    return tmp;
}

let result = factorial(8);
console.log(result); // 40320

И то же самое, только используя рекурсивные вызовы:

// функциональный подход
function factorial(num)
{
    if (num < 0) {
        return -1;
    }
    else if (num == 0) {
        return 1;
    }
    else {
        return (num * factorial(num - 1));
    }
}

let result = factorial(8);
console.log(result); // 40320

Плюсы

  • Читабельный код
  • Простая отладка
  • Неизменяемость и однозначность переменных

Минусы

  • Расточительность
  • Меньшая скорость

Резюме

Так какие, в итоге, имеем преимущества ФП:

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

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

Это был краткий обзор основных принципов и преимуществ функционального программирования, которые будут применяться в примерах дальнейших статей о React и Flux.

Оглавление

  1. Введение
  2. Функциональное программирование
  3. Установка
  4. JSX

Подписка на новости блога: