AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

Давайте начнем понимать основные концепции функционального программирования (ФП) с помощью простых терминов и примеров на JavaScript.

Примечание. Причина выбора JavaScript для демонстрации концепции FP заключается в том, что сам язык рассматривает функцию как первоклассный гражданин (объекты).

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

Иногда элегантная реализация - это просто функция. Не метод. Не класс. Не каркас. Просто функция.

Повестка дня -

  • Основы объектно-ориентированного программирования и функционального программирования
  • Куда они подходят?
  • Основные концепции функционального программирования
  • - Императивный и декларативный стиль
  • - Используйте чистые функции
  • - Воспоминания
  • - Функция высшего порядка
  • - Используйте карту, фильтр и уменьшение
  • - Неизменность
  • - Функциональные концепции
  • - - Функция карри
  • - - Унарная функция
  • - - Частичное применение
  • - - Функция без точек
  • - - Функциональный состав
  • - преобразователь
  • - Функтор и монада
  • - Оптимизация хвостового звонка
  • Резюме

Основы

Объектно-ориентированное программирование

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

Функциональное программирование

  • Данные и поведение - совершенно разные вещи, и для ясности их следует хранить отдельно.
  • Насколько хорошо функции составлены / объединены вместе для обработки данных.

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

Куда они подходят?

Возникает вопрос, где нам нужно использовать FP и OOP? Что делает FP лучше по сравнению с ООП?

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

Ни один из них не лучше другого 🙂

Позвольте мне продемонстрировать формулировку проблемы. Предположим, вам удалось запустить компанию ABC Pvt Ltd. Давайте посмотрим, как мы можем решить эту проблему в обеих парадигмах.

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

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

Когда мы пытаемся стимулировать людей или сущностей, ООП работает хорошо. Например, - Система учета рабочего времени для сотрудника - где нам нужно создать множество объектов для запросов о нерабочем времени, которые прикрепляются к объектам Employee. Где нам требуется сложное взаимодействие данных между объектами. В этом случае лучше всего подходит ООП.

Основные концепции функционального программирования

Императивный и декларативный стиль

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

В декларативном стиле - вы пишете код, который описывает то, что вы хотите, но не обязательно, как это получить (объявляйте желаемые результаты, но не шаг за шагом)

FP побуждают нас использовать декларативный стиль в нашем коде.

Используйте чистые функции

Функция, которая вычисляет свой результат на основе входных данных, называется чистой.

При написании чистых функций следует помнить о двух моментах:

  1. Избегайте побочных эффектов - печать чего-либо на консоли, запись данных в базу данных, запись / чтение из файла, запуск сетевого вызова (Ajax) - вот некоторые примеры побочных эффектов.
  2. Избегайте работы с его внешним состоянием и изменения его собственных аргументов (результат должен зависеть исключительно от его входных аргументов)

FP заставляет нас использовать чистую функцию для вычислений

Потому что чистые функции ссылочно прозрачны. Это означает, что вызов конкретной функции можно заменить ее выводом.

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

Воспоминание

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

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

Функция высшего порядка

Функция может быть входом / выходом функции.

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

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

Использовать карту, фильтровать и уменьшать

Избегайте итератора - вместо ручных итераций с циклом for / while мы можем использовать map, filter и reduce.

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

  1. Обработка параметров итерации и инкрементных счетчиков
  2. Изменение состояния, которое происходит внутри цикла для каждой итерации.

Трудно проверить логику, если мы управляем итерациями, поэтому FP рекомендует нам использовать Array API, например map, filter, reduce, find, some, every и т. Д.

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

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

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

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

Функциональное программирование заставляет нас использовать неизменяемые объекты

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

Что произойдет, если я изменю, объекты будут загрязнены, и мы не знаем, какая функция изменяет состояние объектов (в случае, если многие функции изменяют объекты)

У нас есть некоторые недостатки в использовании неизменяемости

  • Когда нам нужно изменить наши данные h3 -> h4, мы создаем новую копию данных.
  • Чтобы заменить одну комнату h3, нам нужно создать целый новый массив (копируя комнаты h1, h2, даже если мы не меняли).
  • Это простой массив, но как только наш объект или массив становится более сложным, эта методология создает проблемы с эффективностью.

Решение - постоянные структуры данных. Этот термин был первоначально введен Филом Багвеллом в его статье Идеальное хеш-дерево. Затем Рич Хики, который изобрел Clojure, реализовал идею П. Багвелла, чтобы сделать Clojure действительно эффективным для обработки неизменяемых структур данных.

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

Некоторые реализации постоянных структур данных в JavaScript - это Mori и Immutable JS.

Пример для показа спектакля

Функциональные концепции

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

Вам не нужно писать вспомогательные функции для каррирования, унарной и функциональной композиции с нуля. Чтобы использовать эти функциональные возможности, вы можете использовать библиотеку типа Rambda JS.

Преобразователь

Преобразователи - это эффективный конвейер (функции) обработки данных, который не создает промежуточных коллекций.

Когда мы пытаемся связать несколько функций, таких как map, filter и т. Д., В массиве, мы можем в конечном итоге создать несколько промежуточных массивов.

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

Назначение преобразователей - помочь нам избежать этих промежуточных массивов.

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

  • Напишите логику функции map и filter с помощью функции reducer
  • Абстрагируйте логику функции высшего порядка, которую мы передаем карте и фильтру. В приведенном выше примере логика для map - d => d * d, а filter - d => d % 2, мы абстрагируем их, передавая их в качестве аргумента функции.
  • Теперь нам нужно абстрагировать логику сокращения от функций mapWithReducerFn и filterWithReducerFn, что (result, input) => result.concat(input)
mapWithReducerFn(d => d * d)((result, input) => result.concat(input))
filterWithReducerFn(d => d % 2 === 0)((result, input) =>result.concat(input))

Если вы заметите сигнатуру второго аргумента в обеих функциях, которая напоминает сигнатуру функции-редуктора (accumulator,input) => accumulator, и они согласованы.

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

С технической точки зрения преобразователь - это составная функция редуктора более высокого порядка.

Функтор и монада

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

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

Функтор - это то, что можно сопоставить или что-то, что можно сопоставить между объектами в категории.

Функтор - это карта с контекстом.

В приведенном выше примере объектами категории являются source и result. Функция категории - это mappable, а массив - это контекст.

Мы также можем рассматривать объекты как контекст. Пример ниже - это функтор идентичности

Функтор идентичности - это функтор, который возвращает объект и карту / морфизм, которые он потребляет.

// Pseudo code or pattern to represent Identity Functor
const Identity = value => ({
 map: fn => Identity(fn(value))
});

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

Монады выполняют подъем или выравнивание типов и сопоставление с контекстом

В приведенном выше примере нам нужно a -> b -> c, но у нас есть a -> Promise(b) -> b -> Promise(c), поэтому нам нужно что-то, чтобы сгладить контекст от обещания к значению, например. Promise(b) -> b где мы используем монаду для сглаживания composeM метод выполняет сглаживание + отображение контекста на следующую функцию.

Оптимизация хвостового звонка

Вызов функции в конце другой функции - это хвостовой вызов.

Зачем нам нужен хвостовой вызов, потому что хвостовой вызов избегает фреймов стека.

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

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

Резюме

  1. Основы FP - рассматривать данные и поведение отдельно для ясности.
  2. Вариант использования - для работы с данными о людях / сущностях, используйте FP и для стимулирования людей / сущностей, используйте ООП.
  3. FP принуждают нас
  • написать декларативный стиль программирования
  • избегайте побочных эффектов - используйте чистую функцию
  • используйте Memoization - когда ваша чистая функция требует больших вычислительных ресурсов
  • Функция высшего порядка - функция может быть вводом / выводом
  • Избегайте итераторов - используйте карту, фильтруйте и сокращайте
  • Не загрязняйте свой штат - используйте неизменяемые объекты
  • Функциональные концепции - каррирование, унарный, частичное применение, функция без точек, функциональная композиция
  • Преобразователь - технически это функция редуктора высшего порядка.
  • Функтор - это карта с контекстом, а монада - это подъем или сглаживание типов и карта с контекстом.
  • Оптимизация хвостового вызова - вызовите функцию в операторе возврата функции, чтобы избежать кадра стека вызовов.

Надеюсь, вам понравилось читать статью. Я планирую написать больше статей по каждой теме в разделе «Функциональное программирование». Благодарим вас за отзыв, который поможет мне расставить приоритеты. Что еще важнее, не забудьте 👏 😄