Функциональное программирование становится все более актуальным в современной сети. В Paypal мы начали принимать его и уже видим преимущества во многих отношениях. Теперь, когда мы начали погружаться в более «продвинутую» тему, например, линзы для обработки наших глубоко вложенных структур данных, важно, чтобы мы понимали основы.

Что такое функциональное программирование

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

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

Чистые функции - это суть того, что мы делаем. Чистая функция - это просто функция, которая получает один и тот же ввод, всегда возвращает один и тот же вывод и не имеет побочных эффектов. Побочным эффектом можно считать все, что меняет состояние вне функции. Сетевой запрос, запись на диск, изменение переменной в более высокой области, установка значения в уже определенном объекте и т. Д. Простой пример для усвоения:

function add(a, b) {
  return a + b
}
add(1, 2) // 3
add(1, 2) // 3

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

const users = [{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "[email protected]",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
    }
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "Antonette",
    "email": "[email protected]",
    "address": {
      "street": "Victor Plains",
      "suite": "Suite 879",
      "city": "Wisokyburgh",
      "zipcode": "90566-7771",
  }]
function formatUserData (users) {
    return users.reduce(
           (acc, { name, email, username, address }) => 
               acc.concat({ name, username, email, address })
           ,[])
}

Композиция

Композиция - это основной принцип программирования. Если вы писали какой-либо код раньше, вы почти наверняка использовали композицию. Есть несколько различных видов композиции, о которых я не буду сейчас углубляться, но давайте опишем основную идею. Композиция - это способ объединения данных в более сложные данные. Мы часто используем композицию функций (применение результатов одной функции к другой) в функциональном программировании. Например:

function inc(a) {
 return a + 1
}
function double(a) {
  return a * 2;
}
inc(double(2)); // 5

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

Давайте использовать compose.

const compose = (...args) => 
    start => args.reduceRight((acc, fn) => fn(acc), start);

Compose принимает n аргументов. Здесь мы используем параметр отдыха ES6, чтобы собрать аргументы. Затем он возвращает функцию, которая принимает start и возвращает результат уменьшения справа (то же, что и reduce, но начинается с конца массива) и применения каждой переданной функции к аккумулятору.

const addOne = a => a + 1;
const double = a => a * 2;
compose(addOne, double)(2); // 5

Помните, мы читаем это справа налево, как если бы мы назвали это так:

addOne(double(2));

Или, если вы помните из алгебры `f (g (x))`. Извини, у меня есть сочувствие.

Итак, теперь мы увидели, как мы можем составлять функции вместе с отличной compose утилитой. Вам не нужно постоянно писать эту функцию, она доступна в ваших любимых библиотеках утилит, таких как lodash и ramda.

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

const users = [{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "[email protected]",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
    }
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "Antonette",
    "email": "[email protected]",
    "address": {
      "street": "Victor Plains",
      "suite": "Suite 879",
      "city": "Wisokyburgh",
      "zipcode": "90566-7771",
  }]
const compose = (...args) => x => args.reduceRight((acc, fn) => fn(acc), x);
function formatUserData(users) {
    return users.reduce((acc, { name, email, username, address }) =>
                acc.concat({ name, username, email, address }),
           []);
}
const upperCaseName = 
           users => users.reduce(
             (acc, { name, ...rest }) => [
                  ...acc, { 
                  ...rest, 
                  name: name.toUpperCase()
                 }
            ]),[]);
const trimZipCode = users => users.reduce(
         (acc, { address: { zipcode }, ...rest }) => acc.concat(
              {
                ...acc, 
                ...rest, 
                zipcode: zipcode.slice(0,5)
              }
          ), []);
compose(upperCaseName, trimZipCode)(users);

Каррирование и частичное применение

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

const add = a => b => a + b;
add(1)(2); // 3

Довольно просто.

Каррирование позволяет частичное нанесение. Частичное применение - это когда мы применяем к функции меньше аргументов, чем она принимает.

Давайте частично применим add для создания новой функции.

const add = a => b => a + b;
const inc = add(1);

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

Давайте воспользуемся частичным приложением в compose.

// reusing compose from above.
const add = a => b => a + b;
const inc = add(1);
const mult = a => b => a * b;
const double = mult(2);
compose(inc, double)(2); // 5!

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

Без точки - это когда функция не предоставляет аргумент, с которым она ожидает обработки. Возьмем, к примеру, inc. Мы не знаем, каков другой аргумент, в его определении нет a. Есть в add, но inc - это собственная функция, которая просто использует add.

Отсутствие точек может помочь сделать код более читаемым, но вы можете перестараться.

Так что же дальше?

Итак, теперь мы понимаем некоторые из основных принципов функционального программирования. Мы рассмотрели базовую композицию, чистые функции, каррирование и частичное приложение, а также узнали о коде без точек. Что дальше? Лучшее, что можно сделать дальше, - это попрактиковаться. Чем глубже вы углубитесь в ФП, тем больше будут проверены эти основы. По мере того, как вы продолжаете изучать функциональное программирование, важно, чтобы эти основные концепции были понятны и могли быть легко реализованы и объяснены. Моя команда в Paypal выбрала ramda в качестве библиотеки утилит fp. Мы используем его по разным причинам, но в основном склоняемся к нему для композиции линз. Теперь мы можем обрабатывать наши сложные структуры данных в чистом виде, а количество линз многократного использования, которые мы создаем, просто потрясающее! Мы только начинаем, а я уже повторно использую код, который написал неделю назад. Это часть возможностей функционального программирования. Я ленив, я не хочу писать один и тот же код сорок раз.

Спасибо моей команде за то, что они позволяют мне пробовать безумные вещи и терпят меня :).