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

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

Введение

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

Чтобы было ясно: разные проблемы требуют разных инструментов, идеальной и универсальной парадигмы не существует, но существует множество ситуаций, когда FP может принести преимущества. Вот резюме:

  1. сосредоточиться на том, чего вы хотите достичь (декларативно), а не как (обязательно)
  2. более читаемый код, который скрывает бесполезные детали реализации
  3. чистый логический поток , состояние менее рассредоточено и неявно изменено
  4. функции / модули стали легко тестируемыми , многоразовыми и обслуживаемыми
  5. «Безопасный» код без побочных эффектов

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

То же вычисление, тот же результат. Но, как вы можете видеть, императивный код является многословным и не сразу понятным. С другой стороны, декларативный подход читабелен и явен , потому что он фокусируется на том, что мы хотим получить. Представьте себе, что можно распространить ту же разницу на большие части ваших приложений и вернуться к тому же коду через несколько месяцев. Ваше будущее я (и ваши коллеги тоже) оценят этот декларативный стиль!

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

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

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

Функция является чистой, когда:

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

Давайте уточним определение с некоторыми основными примерами.

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

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

Функции как значения

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

В JS мы уже привыкли к этому шаблону (возможно, не сознательно), например, когда мы предоставляем обратный вызов для прослушивателя событий DOM или когда мы используем методы массива, такие как mapreduceили filter.

Давайте снова посмотрим на предыдущий пример:

Здесь mapаргумент представляет собой встроенную анонимную функцию (или лямбду ). Мы можем переписать приведенный выше фрагмент, чтобы более четко продемонстрировать идею «функция как значение», в которой функция userFявно передана map.

Тот факт, что функции в JS являются значениями, позволяет использовать функции высшего порядка (HOF): функции, которые получают другие функции в качестве аргументов и / или возвращают новые функции , часто полученные из функций , полученных в качестве входных данных. HOFs используются для различных целей, таких как специализация и состав функций.

Давайте посмотрим на getHOF. Эта утилита позволяет безопасно и без ошибок получать значения внутренних узлов объектов / массивов (совет: синтаксис ...propsопределен как REST, он используется для сбора списка аргументов в виде массива, сохраненного в параметре с именем props).

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

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

Не нужно многократных (скучных) проверок:

Композиция функций

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

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

Мы filterпользовательский массив, мы генерируем второй с денежными суммами ( map) и, наконец, мы суммируем ( reduce) все значения. Мы составили логику нашей работы в четкой, декларативной и удобочитаемой форме. В то же время мы избегали побочных эффектов, поэтому состояние / среда до и после вызова функции одинаковы.

Помимо ручной композиции, есть утилиты, которые помогают нам составлять функции. Два из них особенно полезны: pipeи compose. Идея проста: мы собираемся объединить n функций, вызывая каждую из них с выводом предыдущей.

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

Давайте проясним идею на простом примере:

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

Неизменность и неизменность подхода

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

Чтобы достичь неизменности в JS, мы должны принять неизменный подход по соглашению, то есть копировать объекты и массивы вместо мутаций «на месте». Другими словами, мы всегда хотим сохранить исходные данные, создавая новые копии .

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

Если мы хотим разорвать все связи с оригиналом, мы должны клонировать, как мы говорим, глубоко . Кажется сложным? Возможно, но потерпите меня несколько минут! ?

Наиболее полезные языковые инструменты для клонирования и обновления структур данных:

  • объект и оператор распространения массива (синтаксис «…»),
  • методы массивов, как отображение, фильтрация и уменьшение. Они оба возвращают мелкую копию.

Вот некоторые операции редактирования, выполняемые с неизменным подходом:

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

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

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

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

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

Общее «глубокое» решение сделано с рекурсивными функциями (которые мы должны взять из библиотек для удобства и надежности). Глубокие копии полезны, если мы хотим быть полностью свободными в манипулировании данными или если мы не доверяем стороннему коду.

Примечание о производительности

Давайте кратко поговорим о производительности . Существуют определенные ситуации, когда неизменность может повысить наши приложения. Например, клон будет размещен в ячейке памяти, отличной от оригинала, что позволяет легко и быстро сравнивать по ссылке. Тот же указатель / ссылка (=== для объектов)? Без изменений. Другая ссылка? Изменения обнаружены, поэтому реагируйте правильно. Нет необходимости внутренних сравнений, потому что мы решили создать отдельные копии для каждого изменения.

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

Полное управление и побочные эффекты

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

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

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

Чистые языки FP имеют тенденцию выдвигать состояние и побочные эффекты на границах приложения, чтобы управлять ими в одном месте. Действительно, функциональное решение этой проблемы заключается в обработке состояния в одном (большом) объекте «вне» приложения , обновленном неизменным подходом (таким образом, клонируется и обновляется каждый раз).

В области внешней разработки этот шаблон применяется и реализуется с помощью так называемых менеджеров состояний, таких как Redux, Vuex и NgRx. За счет увеличения кода (не так много) и сложности наши приложения станут более предсказуемыми, управляемыми и обслуживаемыми.

Вот как работают менеджеры состояний в супер упрощенной диаграмме. События запускают действия, которые активируют редукторы, которые обновляют состояние (хранилище). В результате (в основном) пользовательский интерфейс без сохранения состояния будет обновлен должным образом. Аргументация сложная, но я кратко коснулся темы, чтобы познакомить вас с основной идеей.

3foybl6yynr567567ih1uofzuo Адекватное введение в функциональное программирование

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

Кроме того, этот шаблон позволяет отслеживать мутации . Что мы имеем в виду? Если мы обновляем состояние приложения только с неизменяемыми версиями, мы можем собирать их с течением времени (даже тривиально в массиве). В результате мы можем легко отслеживать изменения и переключаться с одного «состояния» приложения на другое. Эта функция известна как отладка путешествий во времени в Redux-подобных менеджерах состояний.

Выводы

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

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

Добавить комментарий

Войти с помощью: