Привет, Хабр!

Сегодня мы начнём говорить на очень важную тему — функциональное программирование. Значение ФП в современной веб-разработке трудно переоценить. Архитектура любого крупного современного проекта включает в себя пользовательские библиотеки функций и на собеседовании любого уровня в обязательном порядке будут вопросы по ФП.

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


Функциональное программирование(ФП) — способ организации кода через написание набора функций.

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

Отсюда следуют следующие определения:

Функциональный агрумент(Functional argument, фунарг) — аргумент, значением которого является функция.

Функция высшего порядка(ФВП, higher-order-funtion, hof) — функция, которая принимает функции в качестве аргументов.

Функции с функциональным значением(Function valued functions) — функция, которая возвращает функцию.

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

Чистые функции — идеал функционального программирования


Чистые функции (Pure functions, PF) — всегда возвращают предсказуемый результат.
Свойства PF:

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

Пример чистой функции:

const add = (x,y) => x+y;

Хорошим примером нечистоты функции является:

var first;
var second;

function testFn() {
  var a = 10;
  
  first = function() {
    return ++a;
  }

  second = function() {
   return --a;
  }

  a = 2;
  first();//3
}

testFn();

first();//4
second();//3

Представьте сколь усложняется написание тестов для этого примера и сколь оно упрощается для чистых функций!

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

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

Думаю, вы заметили, что в примерах на чистые функции я перешёл на синтаксис ES6. Это было сделано сознательно. Данный синтаксис функций получил название «стрелочные функции»(arrow functions), но на самом деле это реализация математической абстракции, придуманной давным давно. Об этом далее.

Лямбда — функции


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

Термин Лямбда-исчисления ввёл ещё в 1930-х годах Алонзо Черч. По сути лямбда-исчисления не более чем формальная форма описания математического уравнения. Более подробно тут.

В ES на лямбда-функция очень часто реализуют замыкание:

const add = x => y => x + y;

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

Сравните с этим кодом:

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

Очевидно, первый вариант выглядит лучше.

Имутабельность


Неизменяемым (immutable, имутабельность) называется объект, состояние которого не может быть изменено после создания. Результатом любой модификации такого объекта всегда будет новый объект, при этом старый объект не изменится.

Неизменяемость — золотой грааль функционального программирования.

Рассмотрим пример:

const impureAddProp = (key, value, object) => {
  object[key] = value;//Добавляем свойство объекту
};
const User= {
  name: 'Alex'
};
impureAddProp ('isAdmin', true, User);

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

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

const pureAddProp = (key, value, object) => ({
  ...object,
  [key]: value
});
const User= {
  name: 'Alex'
};
const Admin= pureAddProp ('isAdmin', true, User);

Так объект User останется неизменным. Мы изменяем копию данных, а это всегда безопасно.

Заключение


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

Функциональное программирование реализуют многие библиотеки. Это и rambda, и lodash, и многие другие. В реальном проекте вы, разумеется, будете использовать именно их. Под капотом же любых библиотек будет всё тот же нативный javascript, поэтому в следующих статьях мы будем разбирать ФП, реализуя все его концепции именно на нативном JS.

Постскриптум


Начиная писать статьи, я имел ввиду следующий план:

  • писать переводы интересных англоязычных статей
  • осветить несколько актуальных направлений в JS (ключевые концепции, ООП с точки зрения спецификации EcmaScript, паттерны, функциональное программирование).

На сегодняшний день уже написаны головные статьи трёх направлений:

  1. this и ScopeChain в EcmaScript — тут я описал такие ключевые концепции спецификации как контекст исполнения, ключевое слово this и свойство контекста ScopeChain(цепочка областей видимости). В рамках этого направления буквально сегодня вышла моя статья о Лексическом окружении и Замыкании.
  2. Взгляд со стороны EcmaScript на общую теорию ООП — тут была описана разница между статической классовой типизацией и динамической прототипной организацией, разобраны делегирующая модель и утиная типизация
  3. Элегантные паттерны в современном JavaScript (сборная статья по циклу от Bill Sourour) — тут разобраны два паттерна, которые могут пригодиться в каких-то ситуациях. Мой подход в плане паттернов довольно прост: лучше знать как можно больше паттернов, т.к. рано или поздно пригодятся

И вот настала очередь функционального программирования. В дальнейшем я буду писать статьи в продолжении каждого из этих направлений. Например, следующая статья будет о ключевых понятиях ООП: инкапсуляции, абстракции, примесях(и штрихах), интерфейсах и т.д… Также я планирую рассказать о том, как ООП в ES реализовано под капотом, т.е. о свойствах [[Prototype]], [[Class]] и многом другом. Рассказать о том, как v8 создаёт сущности и инстанции классов, функции.

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

В статьях я либо обозреваю концепции, рассказываю как они устроены под капотом( на мой взгляд это улучшает понимание того, что мы пишем и почему пишем именно так), либо рассказываю про какие-то вещи, расширяющие кругозор. На мой взгляд это очень важно. Взгляните на такие компании как Яндекс или Едадил, они постоянно рассказывают о каких-то оригинальных своих идеях. То это битовые карты в реакте, то vue приложение практически полностью на es6 классах. Большинству веб-разработчиков такие вещи просто бы не пришли в голову. Для этого и нужен широкий кругозор.

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

До будущих статей, друзья!

Комментарии (34)


  1. aamonster
    09.11.2019 13:34
    +1

    Интересно терминологию перекосило. Было "functions are first-class citizens", а теперь "functions are first-class functions" (проверил, в англоязычных источниках та же фигня).


    1. Alex_Shcherbackov Автор
      09.11.2019 15:04

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


  1. esata
    09.11.2019 14:23
    +1

    Почему в JS сейчас так форсят так тему ФП?


    1. Graf54r
      09.11.2019 15:56

      потому что ее везде форсят. А форсят ее потому, что она более понятна чем ООП, менее багованый код на ней и легче подается многопоточному программированию.


    1. Aingis
      09.11.2019 16:25

      Потому что про ООП головного мозга все знают (сильно упрощённый в ES2015). Как сказал Ален Кей:

      — Я придумал термин „объектно-ориентированный“, и уверяю вас, что не имел в виду C++
      А про переизбыток ФП ещё не все. Хотя и то, и другое надо уметь использовать, и использовать к месту.


  1. Vlad800
    09.11.2019 14:27

    Извиняюсь, можно вопросы «для чайников в ФП»?

    1) Почему решили писать так:

    const add = (x,y) => x+y;
    const add = x => y => x + y;
    вместо:
    const add(x,y) => x+y;
    const add(x,y) => y => x + y;
    Как по мне, второй вариант как-то единообразнее и привычнее. В этом есть какой-то смысл или просто решили так?
    2) Может я не понимаю, что такое замыкание, но откуда берется Y во втором выражении? Визуально не очевидно, что оно передается в функцию вместе с X.

    3) В примере
    const impureAddProp = (key, value, object) => {
      object[key] = value;//Добавляем свойство объекту
    };
    const User= {
      name: 'Alex'
    };
    impureAddProp ('isAdmin', true, User);
    компилятор будет ругаться?

    4) В примере
    const pureAddProp = (key, value, object) => ({
      ...object,
      [key]: value
    });
    const User= {
      name: 'Alex'
    };
    const Admin= pureAddProp ('isAdmin', true, User);
    разве объект Admin не делит свое состояние с объектом User (имея в виду, что оба они могут быть в реальной жизни намного сложнее)?
    Или «состояние» — это не значение в рантайме, а код в рантайме? Но тогда не менять код объекта в рантайме это разве фича только ФП?


    1. sshikov
      09.11.2019 14:47

      >второй вариант как-то единообразнее и привычнее

      const add(x,y) => x+y;

      const two= 2;

      Во втором случае вы тоже предлагаете выбросить =?


      1. Vlad800
        09.11.2019 14:55

        Вот так вот хочется:
        const two() => 2;


        1. sshikov
          09.11.2019 14:58

          Так я и спрашиваю — для константы, которая число, мы используем const имя = значение, а в этом случае вы предлагаете знак равенства выкинуть. Почему вам это кажется более единообразным, в то время как на самом деле ровно наоборот?


          1. Vlad800
            09.11.2019 15:03

            А если константу (которая число) потом надо будет изменить (по смыслу задачи) на функцию или переменную? Я то вот и думал, что фича ФП как раз в том и состоит, что там все равно: число, переменная или функция — механизм работы с ними один (функция).


            1. sshikov
              09.11.2019 15:14

              Хм. Вообще-то смысл константы в том, что она больше не изменяется. То есть, применяя const вы определяете неизменную константу. И эта константа может быть в том числе и функцией.

              Кстати насчет привычности — в скале, к примеру, все вообще ровно так же, кроме замены const на val. Так что вопрос привычек — он зависит от предыдущего опыта.


              1. Alex_Shcherbackov Автор
                10.11.2019 23:18

                В ФП ничего не изменяется. Концепция имутабельности справедлива для всех сущностей ФП. Об этом и статья.


                1. sshikov
                  11.11.2019 19:16

                  Вы ошибаетесь. ФП не ограничивается иммутабельностью, и вполне бывает без нее. Ну или если угодно, во вполне ФП языках вполне бывают mutable переменные.


    1. Alex_Shcherbackov Автор
      09.11.2019 15:03

      Здравствуйте!

      Код:

      const add(x,y) => y => x + y;
      

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

      Во втором пример Админ не делит состояние с Юзером т.к. свойства Юзера копируются в Админа по значению. В этом и заключается концепция Имутабельности в ФП.

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


    1. justboris
      10.11.2019 01:19

      1) Почему решили писать так:

      Старая добрая запись никуда не делась


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

      ее по-прежнему можно использовать, если штука с const неудобно читается.


      2) Может я не понимаю, что такое замыкание, но откуда берется Y во втором выражении? Визуально не очевидно, что оно передается в функцию вместе с X.

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


      3) В примере компилятор будет ругаться?

      Какой компилятор? В Javascript ругаться точно не будет, но в typescript можно объявить объект неизменяемым и получить ошибку.


      const User = { name: 'Alex' } as const; // неизменяемый объект
      
      User.name = 'Boris'; // Error: Cannot assign to 'name' because it is a read-only property.(2540)
      
      // ошибок компиляции нет 
      const NewUser = { ...User, name: 'Boris' }; 

      Работающий пример.


      Конструкция as const – это специальный синтаксис Typescript, которого в обычном Javascript нет. Документация вот тут.


      4) разве объект Admin не делит свое состояние с объектом User (имея в виду, что оба они могут быть в реальной жизни намного сложнее)?

      Все верно, если у вас внутри есть вложенные объекты или массивы, их тоже нужно не забыть склонировать.


      const user = {
         name: 'Alex',
         location: {
            country: 'RU'
         }
      }
      
      // не забываем склонировать location
      const newUser = { ...user, location: {...user.location}, name: 'Boris' };

      Каждый раз писать такой код может оказаться утомительно, поэтому можно взять утилиту, вроде lodash.cloneDeep.


      1. Alex_Shcherbackov Автор
        10.11.2019 08:42

        Здравствуйте! В рамках статей я сознательно использую нативный js, чтобы показать как та или иная концепция работает. Разумеется, на проекте мы все используем готовые инструменты. Авторы этого коммента я не ответил т.к. очевидно, что он не понял тему.


      1. Vlad800
        11.11.2019 00:00

        Спасибо за развернутый ответ.

        Старая добрая запись никуда не делась… ее по-прежнему можно использовать, если штука с const неудобно читается.
        Читается почти удобно, просто синтаксис кажется несколько избыточным…

        То, что здесь происходит, называется каррированием. Вот тут есть отдельная статья с объяснением, что это такое
        Спасибо, хорошая статья.

        Какой компилятор?
        Извиняюсь, это я из контекста выпал…

        Конструкция as const – это специальный синтаксис Typescript, которого в обычном Javascript нет.
        Приятная конструкция.

        Все верно, если у вас внутри есть вложенные объекты или массивы, их тоже нужно не забыть склонировать.
        Просто в статье было написано «Мы изменяем копию данных, а это всегда безопасно». Но это уже придирки, главное понять что к чему.


    1. mayorovp
      11.11.2019 15:14

      Почему решили писать так

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


      откуда берется Y во втором выражении? Визуально не очевидно, что оно передается в функцию вместе с X.

      Так оно же передаётся не "вместе", а очень даже раздельно. Функция add принимает x и возвращает анонимную функцию, которая уже принимает y.


      Возможно, с лишней парой скобок будет понятнее?


      const add = x => (y => x + y);
      
      const add = x => {
          const addx = y => x + y;
          return addx;
      }


  1. pavelsc
    09.11.2019 15:06
    -1

    const add = x => y => x + y;

    Нет, это не коротко и лаконично, за такое надо руки выравнивать. Особенно когда IDE предлагает add(x) с одним аргументом, и в ответку тебе функция летит. Это очень странно так писать add(1)(2).
    const add = (x,y) => x+y
    А то что приведено выше, обычно используется для инкапсуляции, например так:


    const throttle = action => {
        let isRunning = false;
        return () => {
            if (isRunning) return;
            isRunning = true;
            setTimeout(() => {
                action();
                isRunning = false;
            }, 10000);
        }
    };
    let throttled = throttle(() => console.log(4));
    throttled();throttled();throttled() // callback сработает только один раз

    В примере выше инкапсулируется isRunning. В примере со сложением явный оверинжиниринг


    1. Alex_Shcherbackov Автор
      09.11.2019 15:06

      const add = x => y => x + y;
      


      Это не более чем пример лямбда функции. Иллюстрация концепции.


      1. sshikov
        09.11.2019 15:16

        Двух лямбда функций, я бы сказал…


    1. sshikov
      09.11.2019 15:15

      То что вам это странно — не значит, что это бессмысленно.


    1. sultan99
      10.11.2019 14:16

      const add = x => y => x + y


      Такой прием делается не просто для краткости кода, а как минимум для создания:

      — композиции функций
      — передачи переменных в скоуп функции

      к примеру, есть колбэк функция onClick:

      <button onClick={}>Delete</button>


      и нам необходимо создать универсальную функцию удаления по id, однако функция onClick принимает только один параметр event:

      const onClick = event => {
        event.preventDefault()
        remove(id)
      }


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

      Вот для таких случаях подходит этот прием «функция которая возращает другую функцию»

      
      const deleteOnClick = ({id, remove}) => event => {
        event.preventDefault()
        remove(id)
      }
       
      const DeleteUser = props => (
        <button onClick={deleteOnClick(props)}>
          Delete
        </button>
      )
      
      const DeleteProduct = props => (
        <button onClick={deleteOnClick(props)}>
          Delete
        </button>
      )
      


      и второй случай:

      
      const add = x => y => x + y
      const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)))
      const update = (prop, updater) => obj => ({...obj, [prop]: updater(obj[prop])})
      const now = () => new Date
      
      const likePost = compose(
        update(`date`, now),
        update(`likes`, add(1)),
      )
      
      const post = {id: 1, title: 'Learn FP & love it', date: new Date('2019-01-01'), likes: 130}
      
      likePost(post)
      


      В этом примере мы создаем новую функцию likePost на основе уже двух существующих функции.


  1. strannik_k
    09.11.2019 15:57
    -1

    Например, следующая статья будет о ключевых понятиях ООП: инкапсуляции, абстракции, примесях(и штрихах), интерфейсах и т.д
    И будет очередная статья о том, что уже написано тысячи раз.
    Лучше бы расширили свой кругозор в ООП и написали о том, с чем большинство веб-разработчиков не знакомы.


    1. Alex_Shcherbackov Автор
      09.11.2019 22:18

      Знаете, я начал писать статьи потому что мне в обязанности на работе вменили подтягивать наших джунов)

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


      1. Aingis
        10.11.2019 13:21
        +1

        А почему не дать ссылку на статьи, где это уже давно описано?


        1. Alex_Shcherbackov Автор
          10.11.2019 23:30

          Можно сразу и учебников с десяток «понадовать». Парочку по алгоритмам, парочку по ООП, бессмертный труд Фленагана по яваскрипт, десяток книг по паттернам, матан и введение в ФП и т.д.


  1. FanAs
    11.11.2019 02:06

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


    1. Alex_Shcherbackov Автор
      11.11.2019 08:29

      Приветствую! производительность кода в первую очередь зависит от архитектуры приложения и его алгоритмической составляющей. Грубо говоря, как напишите.

      Например, лишних сущностей можно как в ФП, так и в ООП наплодить.


    1. piton_nsk
      11.11.2019 11:24

      На такой общий вопрос может быть только очень общий ответ. Ответ этот — бывает по разному. Если надо менять много объектов, то иммутабельность может сильно ударить по производительности (я такое видел в реальном проекте). С другой стороны иммутабельность дает бонус для многопопточного кода. Но с третьей стороны, не всякий многопоточный код имеет общее состояние и этот бонус реально нужен. Тут уж все зависит от конкретной задачи.


      1. Alex_Shcherbackov Автор
        11.11.2019 13:34

        Так и есть! я не пропагандирую применять ФП везде и всюду! Это лишь инструмент.

        Молотком, гвоздями и лопатой можно как человека бить, так и сарай на даче построить. Это уже на совести человека, как распорядится инструментами.


        1. piton_nsk
          11.11.2019 14:02

          Это все понятно про инструмент и все такое. Это банальность. ИНтересный вопрос в другом — для каких проектов ФП подходит, а для каких не очень. Хотя бы в теории, а лучше с примерами. Мечты, мечты…


          1. Alex_Shcherbackov Автор
            12.11.2019 08:18
            +1

            На мой взгляд, вопрос стоит не так! Не «для каких проектов», а «для каких частей проекта».

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

            На мой взгляд, удобно.


            1. piton_nsk
              12.11.2019 13:54

              На мой взгляд, вопрос стоит не так! Не «для каких проектов», а «для каких частей проекта».

              Да, так будет точнее сформулировано.