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

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

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

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


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

    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 классах. Большинству веб-разработчиков такие вещи просто бы не пришли в голову. Для этого и нужен широкий кругозор.

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

    До будущих статей, друзья!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +2

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

        0
        Да, поэтому надо держать руку на пульсе, сейчас уже в разговоре с англоязычными разработчиками некоторые термины не употребить.
        +3
        Почему в JS сейчас так форсят так тему ФП?
          –3
          потому что ее везде форсят. А форсят ее потому, что она более понятна чем ООП, менее багованый код на ней и легче подается многопоточному программированию.
          • НЛО прилетело и опубликовало эту надпись здесь
            0
            Извиняюсь, можно вопросы «для чайников в ФП»?

            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 (имея в виду, что оба они могут быть в реальной жизни намного сложнее)?
            Или «состояние» — это не значение в рантайме, а код в рантайме? Но тогда не менять код объекта в рантайме это разве фича только ФП?
              0
              >второй вариант как-то единообразнее и привычнее

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

              const two= 2;

              Во втором случае вы тоже предлагаете выбросить =?
                0
                Вот так вот хочется:
                const two() => 2;
                  +1
                  Так я и спрашиваю — для константы, которая число, мы используем const имя = значение, а в этом случае вы предлагаете знак равенства выкинуть. Почему вам это кажется более единообразным, в то время как на самом деле ровно наоборот?
                    0
                    А если константу (которая число) потом надо будет изменить (по смыслу задачи) на функцию или переменную? Я то вот и думал, что фича ФП как раз в том и состоит, что там все равно: число, переменная или функция — механизм работы с ними один (функция).
                      +1
                      Хм. Вообще-то смысл константы в том, что она больше не изменяется. То есть, применяя const вы определяете неизменную константу. И эта константа может быть в том числе и функцией.

                      Кстати насчет привычности — в скале, к примеру, все вообще ровно так же, кроме замены const на val. Так что вопрос привычек — он зависит от предыдущего опыта.
                        –1
                        В ФП ничего не изменяется. Концепция имутабельности справедлива для всех сущностей ФП. Об этом и статья.
                          0
                          Вы ошибаетесь. ФП не ограничивается иммутабельностью, и вполне бывает без нее. Ну или если угодно, во вполне ФП языках вполне бывают mutable переменные.
                0
                Здравствуйте!

                Код:
                const add(x,y) => y => x + y;
                

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

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

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

                  0
                  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.

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

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

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

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

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

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

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

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


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

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


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


                      const add = x => (y => x + y);
                      
                      const add = x => {
                          const addx = y => x + y;
                          return addx;
                      }
                      –2
                      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. В примере со сложением явный оверинжиниринг

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


                        Это не более чем пример лямбда функции. Иллюстрация концепции.
                          0
                          Двух лямбда функций, я бы сказал…
                          +1
                          То что вам это странно — не значит, что это бессмысленно.
                            0
                            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
                            Например, следующая статья будет о ключевых понятиях ООП: инкапсуляции, абстракции, примесях(и штрихах), интерфейсах и т.д
                            И будет очередная статья о том, что уже написано тысячи раз.
                            Лучше бы расширили свой кругозор в ООП и написали о том, с чем большинство веб-разработчиков не знакомы.
                              0
                              Знаете, я начал писать статьи потому что мне в обязанности на работе вменили подтягивать наших джунов)

                              Я просто вижу в каких вещах у разработчиков определённого уровня пробелы и решил написать цикл статей, закрывающих эти пробелы. Убить двух зайцев так сказать.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  –2
                                  Можно сразу и учебников с десяток «понадовать». Парочку по алгоритмам, парочку по ООП, бессмертный труд Фленагана по яваскрипт, десяток книг по паттернам, матан и введение в ФП и т.д.

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

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

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

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

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

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

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

                                Самое читаемое