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

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

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

    Итак, начнём!

    Предположим, перед нами стоит задача: создать набор инструментов для работы с палиндромами.
    ПАЛИНДРО́М
    Мужской родСПЕЦИАЛЬНОЕ
    Слово или фраза, которые одинаково читаются слева направо и справа налево.
    «П. «Я иду с мечем судия»»
    Одна из возможных реализаций данной задачи могла бы выглядеть так:

    function getPalindrom (str) {
      const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g;
      str = str.replace(regexp, '').toLowerCase().split('').reverse().join('');
      //далее какой-то аякс запрос в словарь или к логике, которая генерирует фразы по переданным буквам
    
      return str;
    }
    
    function isPalindrom (str) {
      const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g;
      str = str.replace(regexp, '').toLowerCase();
      return str === str.split('').reverse().join('');
    }
    

    Безусловно, эта реализация работает. Мы можем ожидать, что getPalindrom будет работать корректно если апи будет возвращать корректные данные. Вызов isPalindrom('Я иду с мечем судия') вернёт true, а вызов isPalindrom('не палиндром') вернёт false. Хороша ли эта реализация с точки зрения идеалов функционального программирования? Определённо, не хороша!

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

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

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

    Вспомним определение: Функциональное программирование — способ написания кода через составления набора функций.

    Составим набор функций для этой задачи:

    const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1)
    const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2)
    const toLowerCase = str => str.toLowerCase();//(3)
    const stringReverse = str => str.split('').reverse().join('');//(4)
    const isStringsEqual = (strA, strB) => strA === strB;//(5)
    

    В строке 1 мы объявили константу регулярного выражения в функциональной форме. Такой способ описания констант часто применяется в ФП. Во 2-й строке мы инкапсулировали метод String.prototype.replace в функциональную абстракцию replace, для того чтобы он(вызов replace) соответствовал контракту функционального программирования. В 3-й строке идентичным образом создали абстракцию для String.prototype.toLowerCase. В 4-й реализовали функцию создающую новую развёрнутую строку из переданной. 5-я проверяет на равенство строки.

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

    Теперь нам необходимо реализовать проверку — является ли строка палиндромом. На помощь нам придёт композиция функций.

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

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

    Мы можем сделать так:

    isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', 'Я иду с мечем судия')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', 'Я иду с мечем судия'))));
    

    или вот так:

    const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', 'Я иду с мечем судия'));
    const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', 'Я иду с мечем судия')));
    console.log(isStringsEqual(strA, strB));
    

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

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

    const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
    

    Функция compose принимает в качестве аргументов список выполняемых функций, превращает их в массив, сохраняет его в замыкании и возвращает функцию, которая ожидает начальное значение. После того как начальное значение передано, запускается последовательное выполнение всех функций из массива fns. Аргументом первой функции будет переданное начальное значение x, а аргументами всех последующих будет результат выполнения предыдущей. Так мы сможем создавать композиции любого числа функций.

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

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

    const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
    

    ожидает принять 3 параметра на вход, а мы в compose передаём только один. Решить эту проблему нам поможет другая техника ФП — Каррирование.

    Каррирование — преобразование функции от многих аргументов к функции от одного аргумента.

    Помните нашу функцию add из первой статьи?

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

    Её можно каррировать так:

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

    Функция принимает x и возвращает лямбду, которая ожидает y и выполняет действие.

    Преимущества каррирования:

    • код выглядит лучше;
    • каррированные функции всегда чисты.

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

    const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str);
    

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

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

    В нашем случае мы создали функцию replaceAllNotWordSymbolsGlobal, которая является частично примененным вариантом replace. Она принимает replacement, сохраняет его в замыкании и ожидает на вход строку для которой вызовет replace, а аргумент regexp мы закрепляем константтой.

    Вернёмся к палиндромам. Создадим композицию функций для палиндромовой сроки:

    const processFormPalindrom = compose(
      replaceAllNotWordSymbolsGlobal(''),
      toLowerCase,
      stringReverse
    );
    

    и композицию функций для строки с которой будем сравнивать потенциальный палиндром:

    const processFormTestString = compose(
      replaceAllNotWordSymbolsGlobal(''),
      toLowerCase,
    );
    

    теперь вспомним то, что мы говорили выше:
    типичный пример композиции — передача вызова одной функции в качестве аргумента другой
    и напишем:

    const testString = 'Я иду с мечем судия';//в данном случае я не стал переписывать коснтанту в функциональном стиле, т.к. подразумеваю, что это не константа, а значение, которое приходит откуда-то из внешнего кода, например с сервера
    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
    

    Вот мы получили рабочее и неплохо выглядящее решение:

    const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;
    const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
    const toLowerCase = str => str.toLowerCase();
    const stringReverse = str => str.split('').reverse().join('');
    const isStringsEqual = (strA, strB) => strA === strB;
    
    const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str);
    
    const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
    
    const processFormPalindrom = compose(
      replaceAllNotWordSymbolsGlobal(''),
      toLowerCase,
      stringReverse
    );
    
    const processFormTestString = compose(
      replaceAllNotWordSymbolsGlobal(''),
      toLowerCase,
    );
    
    const testString = 'Я иду с мечем судия';
    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
    

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

    const curry = fn => (...args) => {
      if (fn.length > args.length) {
        const f = fn.bind(null, ...args);
        return curry(f);
      } else {
        return fn(...args)
      }
    }
    

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

    Также мы можем создать частично применённую функцию для замены нужного нам регулярного выражения на пустую строку:

    const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), '');
    

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

    const party = (fn, x) => (...args) => fn(x, ...args);
    

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

    Теперь перепишем party так, чтобы мы могли создать частично применённую функцию от нескольких аргументов:

    const party = (fn, ...args) => (...rest) => fn(...args.concat(rest));
    

    Отдельно стоит отметить, что каррированные таким способом функции могут быть вызваны с любым количеством аргументов меньше задикларированного(fn.length).

    const sum = (a,b,c,d) => a+b+c+d;
    const fn = curry(sum);
    
    const r1 = fn(1,2,3,4);//очевидно, рабочий пример
    const r2 = fn(1, 2, 3)(4);//этот и все последующие также будут работать
    const r3 = fn(1, 2)(3)(4);
    const r4 = fn(1)(2)(3)(4);
    const r5 = fn(1)(2, 3, 4);
    const r6 = fn(1)(2)(3, 4);
    const r7 = fn(1, 2)(3, 4);
    

    Вернёмся к нашим палиндромам. Мы можем переписать нашу replaceAllNotWordSymbolsToEmpltyGlobal без лишних скобок:

    const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), '');
    

    Давайте посмотрим на весь код:

    //Набор функций может лежать где-то в бандле по утилям для строк 
    const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;
    const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
    const toLowerCase = str => str.toLowerCase();
    const stringReverse = str => str.split('').reverse().join('');
    const isStringsEqual = (strA, strB) => strA === strB;
    
    //Строки могут приходить откуда нибудь с сервера
    const testString = 'Я иду с мечем судия';
    
    //инструменты функционального программирования могут лежать в отдельном бандле или можно использовать какую-нибудь функциональную библиотеку типа rambda.js
    
    const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
    
    const curry = fn => (...args) => {
      if (fn.length > args.length) {
        const f = fn.bind(null, ...args);
        return curry(f);
      } else {
        return fn(...args)
      }
    }
    
    const party = (fn, ...args) => (...rest) => fn(...args.concat(rest));
    
    
    //и вот он бандл с нашей программой
    const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), '');
    
    const processFormPalindrom = compose(
      replaceAllNotWordSymbolsToEmpltyGlobal,
      toLowerCase,
      stringReverse
    );
    
    const processFormTestString = compose(
      replaceAllNotWordSymbolsToEmpltyGlobal,
      toLowerCase,
    );
    
    const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
    
    

    Выглядит здорово, но вдруг нам не строка, а массив придёт? Поэтому допишем ещё одну функцию:

    const map = fn => (...args) => args.map(fn);
    

    Теперь если у нас будет массив для тестирования на палиндромы, то:

    const palindroms = ['Я иду с мечем судия','А к долу лодка','Кит на море романтик'. 'Не палиндром']
    
    map(checkPalindrom )(...palindroms ); // [true, true, true, false] работает корректно
    

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

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

    В функциональных библиотеках, например ramda.js есть функции compose и pipe. compose реализует алгоритм композиции справа налево, а pipe слева направо. Наша функция compose это аналог pipe из ramda. В библиотеке две разных функции композирования т.к. композиция справа-налево и слева-направо это два разных контракта функционального программирования. Если кто-то из читателей найдёт статью, в которой описаны все существующие контракты ФП, то поделитесь ей в комментах, с радостью почитаю и плюсик коменту поставлю!

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

    Заключение


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

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

    К слову, вы можете продолжить развитие этого модуля работы с палиндромами и развить его идеи, например загружать строки по апи, преобразовывать в наборы букв и отправлять на сервер, где будет генерироваться строка палиндром и многое другое… На ваше усмотрение.

    Также неплохо было бы избавится от дублирования в процессах этих строк:

      replaceAllNotWordSymbolsToEmpltyGlobal,
      toLowerCase,
    

    В общем, совершенствовать код можно и нужно постоянно!

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

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

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

      +2
      «иду с мечем»? Нет, вот правда?
        0

        Я разуму уму заря,
        Я иду с мечем судия;
        С начала та ж я и с конца
        И всеми чтуся за Отца.


        Державин, 1805г


        Что вас смущает?

          0
          Только то, что мы сейчас в 2019 году. И сейчас либо «мечём», либо «мечом». И в обоих случаях фраза уже не палиндром. Просто для примера можно было бы выбрать однозначно читающийся палиндром, а таких полно, хоть про Азора и его лапу ))
          оффтопик
          Любопытно, что у Державина была-то в оригинале «ё» наверняка, он был среди тех, кто её продвигал. А потерялась она при перепечатке, уже в наше время.
            0

            Какая разница, какой сейчас год если фраза написана 200 лет назад? А данная фраза широко известна и знаменита тем, что это первый достоверно известный авторский палиндром на русском. Потому и является отличным примером.


            Любопытно, что у Державина была-то в оригинале «ё»

            Вы в курсе, что в русском допустимо вместо "ё" писать "е"? У Державина там еще и "ъ" были.

              +1
              Вы в курсе, что в русском допустимо вместо «ё» писать «е»?

              Вы про современный русский? А «мечем» вместо «мечом» тоже допустимо? Закругляюсь, полнейший оффтопик, свою т.з. я высказал, к теме публикации всё это отношения не имеет.
              0
              Я извиняюсь! Как бы это иллюстративный пример, речь шла о программировании, а не о русском языке. Фразу я взял с тестов к задаче, которую мы даём на собеседовании. Как она туда попала я не знаю, уж извините.
                0
                С тестов?
                  0
                  Ну да, выдернул из теста. В тесты она видим попала из определения палиндрома.
                    0
                    Другое дело.
          +3
          Это все забавно, но изначальный вариант выглядит и читается намного проще ОО.
          Особенно если чуть порефакторить, чтобы избежать дублирования с регуляркой
            0
            Прочитайте ТЗ пожалуйста. Задача создать набор инструментов, а не только две функции, т.е. мы не знаем точно как мы будем использовать элементы набора в коде, поэтому в итоге я приводил весь код с условным разделением на бандлы. На мой взгляд, в реалиях тех задач, которые были поставлены набор абстрактных функций выглядит лучше, чем 2 конкретные функции.

            Если вы считаете иначе, то поясните пожалуйста.
              +2
              Ну вы конечно получили инструмент для работы с любым количеством любых функций, но инструмент не делает полезной работы. Вам нужно описать функции, которые делают реальную работу и потом прогнать их через ваш инструмент. Каков итог? у вас больше кода, он более абстрактный и непонятный. Тогда зачем все это шаманство?
                0
                Ну так я и описал функции, которые делают реальную работу.

                const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;
                const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
                const toLowerCase = str => str.toLowerCase();
                const stringReverse = str => str.split('').reverse().join('');
                const isStringsEqual = (strA, strB) => strA === strB;
                


                Это пример хелпера для каких либо целей.

                Далее мы используя ФП инкапсулируем определённую бизнес логику в продуктовые функции.

                Опять же, данный код не более чем обучающий пример на темы композиция, каррирование, частичное применение. В качестве допущения для данной задачи было выбрано: необходимость достижения функциональной чистоты. Кстати, вполне вероятная задача на собеседованиях.
                  +2
                  Нет-нет. Вы усложнили функции, чтобы они подошли под инструмент.
                  Вот к примеру:
                  const toLowerCase = str => str.toLowerCase();
                  В чем смысл? Никакой дополнительной ценности, кроме того, что можно юзать с вашим новым инструментом. Но код вы пишите для инструмента или для решения проблем?
                    0
                    Это переход к функциональному стилю, т.е. к бесточечному коду на верхнем уровне. Довольно распространённая практика.

                    Вот почитайте.

                    Такой подход очень часто применяется в совокупности с теми концепциями, которые я описал в статье.
                      0
                      IMHO, гораздо интересней и проще использовать пайп + object in — object out
                        0
                        Вы опять же не поняли. Разумеется, на проекте вы не будете реализовать сами curry, partial и прочее, а просто подключите библиотеку.

                        В статье я реализовал всё нативно для того чтобы показать как эти вещи устроены под капотом.

                        Цитата из статьи:
                        Заключение

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

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

                        К слову, вы можете продолжить развитие этого модуля работы с палиндромами и развить его идеи, например загружать строки по апи, преобразовывать в наборы букв и отправлять на сервер, где будет генерироваться строка палиндром и многое другое… На ваше усмотрение.

                        Также неплохо было бы избавится от дублирования в процессах этих строк:

                        replaceAllNotWordSymbolsToEmpltyGlobal,
                        toLowerCase,

                        В общем, совершенствовать код можно и нужно постоянно!
                        0
                        т.е. к бесточечному коду на верхнем уровне

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

                          0
                          И тем не менее, при изучении ФП данный стиль необходим показать.
              +3
              Какое же уродство, просто жесть
                0
                Поясните?
                  0
                  Вы на полном серьезе считаете что написать код в императивном стиле (который на порядок легче и быстрее читается и понимается, просто сверху вниз, слева на право) это хуже, чем функциональщина которую вы преподнесли?
                    0
                    Цель данной статьи: проиллюстрировать некоторые концепции Функционального программирования. Для достижения этой цели мною был выбран следующий метод: на примере популярной задачи из собеседования показать тонкости этих концепций(композиция, каррирование, частичное применение). Поэтому постановка задача была сформулирована так: создайте набор инструментов для решения проблемы. Данная постановка исключает императивный стиль в принципе.
                +2

                Можно побыть занудой?
                В регекспах в квадратных скобках совсем другой синтаксис.
                * экранировать не нужно, . — тоже.
                ^ если стоит не первым символом — тоже.
                - не нужно экранировать, если поставить сразу после открывающей скобки или перед закрывающей.


                Регекспы и так тяжело читать. А когда в них экранируется что нужно и не нужно — тем более.


                P.S. И небольшое grammar-занудство: правильно areStringsEqual потому что to be во множественном числе становится are.

                  –1
                  Да это так, регулярку я просто загуглил, она тут не столь важна.
                  +1
                  Во-вторых, функция getPalindrom обращается к базе.

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


                  К слову, аналог getPalindrom в функциональном стиле вы так и не написали.


                  В-третьих, функции модифицируют свои аргументы. Итого, наши функции не чисты.

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

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

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

                      Вот именно. А вы почему-то пишите, что текущая реализация плоха с точки зрения ФП, потому что там идёт обращение к базе. Но если логика обращения к базе в этой самой функции, то она плоха в принципе, не взирая ни на какую парадигму.


                      И вообще, о чём эта статья? Показать плюсы ФП? Ну что ж, вместо функции на 3-5 строк у вас вышло кода в 3-4 раза больше, он читается труднее. Не понятно, зачем всё переписывать в функциональном стиле.


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

                        0
                        Статья о трёх концепциях ФП. Как видите, я реализовал композицию, каррирование и частичное применение нативными средствами. Остальное лишь иллюстративный пример. Данный код нужно воспринимать как код задачи.

                        На проекте всё слишком специфично и зависит от архитектуры. Для каких — то проектов организация кода в статье — антиреклами ФП, а для каких-то очень даже уместно.

                        Задача в статье поставлена так: создайте набор инструментов! Не было сказано «напишите две функции». Набор инструментов подразумевает, что мы должны получить на выходе апи, с помощью которого сможем организовать любой тех процесс работы с палиндромами. Собственно поэтому я и показал примерное разбиение на бандлы. В сравнении с начальным вариантом код результата более переиспользуемый и чистый. С точки зрения поставленной задачи код вполне хорош.
                    +1

                    Как мне кажется, преимущества ФП надо показывать на тех примерах, где ФП действительно помогает. А сейчас, получается, натянули сову на глобус.
                    Видел я один проект, который полностью написанн в функциональном стиле функциональной версии lodash. Прямо Lodash Головного Мозга. И, честно говоря, от проекта остались ровно такие же ощущения, как от этой статьи: вместо простых и понятных функций получаем ворох функций, функций-аргументов, функций-связок, функций-функций. Да, все чисто, по канону, но не удобно для чтения, а порой и поддержки.

                      0
                      Это надо рассматривать как учебную задачу.
                      +1
                      Вы очень упорный человек — упорно пишите «palindrom» вместо «palindrome», упорно набиваете десятки чистых функций, где можно обойтись одним циклом без доп. памяти! ~~Респект таким парням~~ Честь и хвала таким людям!
                        0
                        Понимаете, ничего не существует в вакууме. Применительно к программированию любой код служит определённым целям. Данный код и данный пример преследует учебные цели: проиллюстрировать определённые техники и практики.

                        На реальных проектах другие законы. Люди опытные в функциональном программировании не будут читать такие простые статьи, они им не нужны, а для постигающих эти концепции будет простой и понятный пример, который, кстати, может встретится на собеседовании.
                        0
                        Для меня лично функциональное программирование в JS заканчивается на применении его огрызков в виде forEach/map/reduce/filter/every/some. Сильно не люблю код с циклами и с переменными-счётчиками, которые можно заменить использованием этих функций.

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

                        Мысль писать весь код в функциональном стиле вызывает… сильное отторжение. Подозреваю, что это результат воспитания и образования, когда серьёзно погружаться в программирование начинаешь с pascal/c++. Возможно, поколение, которое только-только начинает изучать программирование, при наличии всех современных языков, будет иначе воспринимать ФП.

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

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

                        P.S. Совсем иначе эту статью можно воспринимать, если в примерах кода начать применять pipeline оператор из ES2019 proposal github.com/tc39/proposal-pipeline-operator
                        С ним чистое ФП или его отдельные элементы выглядят совсем иначе, гораздо понятнее и лаконичнее.

                        P.P.S В подобных статьях имхо стоит через строчку писать, что это упрощенный учебный пример.
                          –1
                          ФП очень хорошо подходит для организации утилей и хелперов на реальных проектах. Разумеется бизнес логику на ФП не нужно организовывать, а вот «набор инструментов для той или иной задачи» в хелперах или утилях будет правильным реализовать на ФП.
                            +1
                            Разумеется бизнес логику на ФП не нужно организовывать

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

                              0
                              А можно предметную область хоть как-то обозначить?
                                0

                                Ну в нашем случае это backend для соц. сети. А вообще не принципиально.

                                  0
                                  А в чем преимущество ФЯП перед «обычными» ЯП (в тех предметных областях, про которые вы знаете)? Надежней, легче масштабировать, или что-то другое? Именно плюсы, о которых говорят сами разработчики…
                                    0

                                    Ну это не меня надо спрашивать, на самом деле. Я ФП использую лишь отчасти. Я полагаю что киллер фича — декларативность. Есть строгая чёткая схема того как и почему что происходит. А те места где творится магия вынесены в свои собственные загончики, которые явным образом помечены. Это как unsafe в Rust. Проще дебажить, больше гарантий уже на уровне компиляции, понимание что сюрпризов будет куда меньше, и, как часто говоря ФП разработчики, больше дисциплины, т.к. эти искусственные ограничения принуждают разделять мух от котлет.


                                    На на самом деле вам лучше спросить тех кто на ФП языках пишет. Я "не настоящий сварщик" :)

                                      0
                                      Все равно спасибо за ответ :)
                                      0

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


                                      Но это не относится никаким боком к императивным языкам, в которых можно писать в функциональном стиле.


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


                                      Потому писать в функциональном стиле на императивных языках можно, но каких-либо особенных преимуществ это не дает. Иногда код получается проще, но не всегда (см. статью про собеседование в Яндекс, где превозносится нечитаемая лапша с map/filter).

                                        0
                                        Так а разве код, написанный в функциональном стиле в императивном языке, трудно распараллелить? Если только его параллелить… Хотя, это уже может зависеть от конкретного ЯП (интерпретируемый-компилируемый), его компилятора и т.д.

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

                                        Статью пропустил, надо почитать…
                            0
                            Свойства PF:… Не используют глобальные значения

                            Pure function может использовать глобалки. Никаких проблем с этим пока они иммутабельны. Откуда вы это взяли?


                            В-третьих, функции модифицируют свои аргументы

                            function isPalindrom (str) {
                              const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g;
                              str = str.replace(regexp, '').toLowerCase();
                              return str === str.split('').reverse().join('');
                            }

                            Где вы тут модифицируете аргументы? str = str.replace не модифицирует аргумент. Ваш аргумент остался нетронутым, вы просто потеряли с ним связь. Некрасиво с точки зрения ФП и чистоты кода, но не более того. И .reverse() тоже ничего не нарушает, ибо мутирует созданную в пределах вашей функции сущность (.split()). Эта функция не нарушает правил pure functions. Хотя конечно же лучше было присвоить это значение другому наименованию.

                              0
                              const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1)

                              Что это? Зачем вы статику засунули в метод? Что мешало сделать стандартно: const BLABLA = /blabla/?


                              const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2)
                              const toLowerCase = str => str.toLowerCase();//(3)

                              Этим вы только хуже сделали. До — было просто и понятно, после — требуется лезть в код и смотреть что же все эти методы делают. Смысл избавляться от точечной нотации в мультипарадигменном языке? да ещё и в стандартной библиотеке...


                              const isStringsEqual = (strA, strB) => strA === strB;//(5)

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


                              ФП оно не про то как создать 100500 однострочных методов делающих очевидные вещи. ФП призван упростить сложное за счёт более понятной схемы взаимодействия и трансформации данных. В вашем примере вы взяли простое решение (причём isPalindrom уже был PF) и раздули его до груды кода, которая не должна пройти code review.


                              Поэтому в вас столько негативных комментариев.

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

                                Применительно к программированию любой код служит определённым целям. Данный код и данный пример преследует учебные цели: проиллюстрировать определённые техники и практики.

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

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

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