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

Алан-э-Дейл       25.12.2022 г.

Что это

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

Функциональное программирование, несмотря на кажущуюся сложность, несёт в себе ряд преимуществ:

  1. Код становится короче;
  2. Понятнее;
  3. Включает в себя признаки хороших императивных языков: модульность, типизация, чистота кода.

Примерами функциональных языков являются LISP (Clojure), Haskell, Scala, R. В общем-то, вы даже можете попробовать писать функциональный код на Python или Ruby, но это больше развлечение для мозгов, нежели рациональное использование возможностей языка.

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

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

Конференция Design is Frontend

14 сентября в 18:00, Онлайн, Беcплатно

tproger.ru

События и курсы на tproger.ru

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

Java

Kotlin

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

Java

Kotlin

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

Что такое ООП?

Я подойду к вопросу с редукционистских позиций. Есть много правильных определений ООП которые покрывают множество концепций, принципов, техник, паттернов и философий. Я намерен проигнорировать их и сосредоточиться на самой соли. Редукционизм тут нужен из-за того, что всё это богатство возможностей, окружающее ООП на самом деле не является чем-то специфичным для ООП; это просто часть богатства возможностей встречающихся в разработке программного обеспечения в целом. Тут я сосредоточусь на части ООП, которая является определяющей и неустранимой.

Посмотрите на два выражения:

1: f(o); 2: o.f();

В чём разница?

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

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

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

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

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

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

Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.

Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.

Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f(o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.

Однако, когда мы смотрим на Выражение 2. o.f() мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.

Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.

Итак, наше редукционистское определение ООП это:

Структурное программирование

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

Чтобы решить эту проблему, Дейсктра решил сделать написание программ подобно математическим доказательствам, которые также организованы в иерархии. Он понял, что если в программах использовать только if, do, while, то тогда такие программы можно легко рекурсивно разделять на более мелкие единицы, которые в свою очередь уже легко доказуемы.

С тех пор оператора goto не стало практически ни в одном языке программирования.

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

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

Elm

Один из новых языков в этом списке, Elm-чисто функциональный язык, первоначально разработанный Evan Czaplicki в 2012 году. Язык приобрел популярность среди веб-разработчиков, в частности, для создания пользовательских интерфейсов.

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

Компилятор Elm предназначен для HTML, CSS и JavaScript. Так же, как вы можете использовать Clojure для написания программ, которые работают на Java, вы можете писать приложения, которые используют библиотеки JavaScript в Elm.

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

Clojure

В отличие от JavaScript и Python, Clojure может быть не совсем знакомым языком, даже среди программистов. В случае, если вы не знакомы с Clojure – этот язык является диалектом языка программирования Lisp, который придумали к концу 1950-х годов.

Как и другие диалекты Lisp, Clojure рассматривает код как данные. Это означает, что код может эффективно изменять себя. В отличие от других диалектов Lisp, Clojure работает на платформе Java и компилируется в байт-код JVM. Это означает, что он может работать с библиотеками Java, были ли они написаны на Clojure или нет.

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

Реализация

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

Замечательный факт №2. В стандартной библиотеке (C++17) есть почти всё необходимое для реализации частичного применения по данной модели.

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

Итак, нам понадобятся (полный и исчерпывающий список):

  1. std::forward
  2. std::move
  3. std::forward_as_tuple
  4. std::apply
  5. std::invoke
  6. std::tuple_cat
  7. std::make_tuple

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

Теперь можно с уверенностью говорить, что всё необходимое у нас уже имеется. Можно писать код.

  1. Функция

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

  2. Структура

    1. Хранит частично применённые объекты: функцию и первые её аргументов.

    2. Имеет оператор «скобочки», который принимает произвольное количество аргументов

      1. При вызове формируется кортеж ссылок на входные аргументы при помощи функции .
      2. Кортеж с сохранёнными ранее объектами также преобразуется в кортеж ссылок при помощи определённой нами функции .
      3. Оба кортежа ссылок склеиваются в один при помощи функции . Получается один большой кортеж ссылок.
      4. Склеенный кортеж разворачивается и передаётся в функцию при помощи функции .
      5. Вызов функции от полученных аргументов.

Всё.

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

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

Использование

Благодаря вызову поддерживаются :

А благодаря использованию функции будут поддерживаться различные сложные случаи, такие как вызов метода класса (см. std::invoke) и т.п.

И всё это «из коробки» и без каких-либо специальных телодвижений с нашей стороны.

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

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

  • Чистые функции — функциональное программирование использует чистые функции. Это функции, которые не изменяются, дают надёжные результаты и всегда дают одинаковый результат для одного и того же ввода. Они не вызывают неожиданных результатов или побочных эффектов и абсолютно предсказуемы независимо от внешнего кода.
  • Неизменяемость — это принцип, согласно которому после того, как вы установили значение для чего-либо, это значение не изменится. Это устраняет побочные эффекты или неожиданные результаты, поскольку программа не зависит от состояния. Таким образом, функции всегда работают одинаково при каждом запуске; это чистые функции.
  • Дисциплинированное состояние — новые ценности могут быть созданы, поэтому есть некоторые состояния, которые могут измениться в этом смысле, но это глубоко контролируемый процесс. Функциональное программирование стремится избежать общего состояния и изменчивости. Если состояние жёстко контролируется, его легче масштабировать и отлаживать, и вы получаете менее неожиданные результаты.
  • Ссылочная прозрачность — этот принцип основан на сочетании чистых функций и неизменности. Поскольку наши функции чисты и предсказуемы, мы можем использовать их для замены переменных и, следовательно, уменьшить количество выполняемых назначений. Если результат функции будет равен переменной, поскольку наши результаты предсказуемы, мы можем просто заменить переменную этой функцией.
  • Функции первого класса — этот принцип прост. Функциональное программирование очень высоко ценит определённые функции, функции первого класса. Следовательно, он поддерживает передачу целых функций между собой так же легко, как другие языки с переменными. Эти функции можно рассматривать как значения или данные в функциональном программировании.
  • Системы типов — поскольку функциональное программирование так сосредоточено на точности и предотвращении ошибок. Наличие статически типизированной системы имеет смысл. Это необходимо для того, чтобы убедиться, что каждый тип данных назначен правильно, строки — это строки, а числа с плавающей запятой — это числа с плавающей запятой, и предотвращает использование непредсказуемых переменных.

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

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

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

Рекомендации по ФП на языке Python

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

Что делает функции нечистыми?

  • Глобальные мутации, т.е. внесение изменений в глобальное состояние,

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

  • Операции ввода-вывода.

Пример глобальной мутации:

Пример недетерминированности:

Пример операции ввода-вывода:

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

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

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

В отличие от объектно-ориентированного программирования, которое строит сложные формы поведения с помощью наследования, ФП опирается на композицию функций. Этот принцип перекликается с философией Unix, состоящей из 2 правил:

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

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

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

Пример класса с мутирующим интерфейсом:

Пример класса без мутирующего интерфейса:

Но лучше использовать замороженные dataclasses и копирование, где необходимо. Иными словами, все классы должны быть замороженными dataclasses.

При всем при этом dataclasses могут быть вполне себе умными!

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

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

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

Зачем это нужно

Наверное — самый главный вопрос. Зачем так мучиться? Копировать данные вместо того чтобы изменить напрямую, оборачивать объекты в эти ваши чтобы изменения (если они есть) не утекали наружу, и вот это всё… Ответ — для лучшей композиции. В своё время очень невзлюбили именно потому, что с ним очень трудно понять как на самом деле программа себя ведет и какой на самом деле поток данных и управления, и переиспользовать функцию написанную с было сложно, ведь тогда он умел даже в середину тела функции прыгнуть без каких-либо проблем.

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

В качестве примера почему это хорошо могу привести случай из жизни который случился как раз со мной пару месяцев назад. Писал я самый типовой ООП код на C#, и понадобилось мне влезть в старый кусок, где был написан вот такой код (пример упрощён)

И понадобилось мне во время выполнения задачи их немного отрефакторить, что я и сделал:

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

Соответственно если раньше с точки зрения компилятора оно выглядело так:

То после рефакторинга получилось следующее:

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

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

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

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

Установка IDE и SBT

Чтобы использовать Scala под платформу Java, вам понадобятся установленные JRE и JDK. Для знакомства с языком разумно использовать IDE IntelliJ IDEA Community Edition со специальным плагином Scala Plugin (доступен прямо при установке IDE) и систему построения проектов SBT (есть под большинство ОС, скачивается при создании проекта).

Примеры из статьи можно запускать, используя онлайн-компилятор Scalastie.

Для того чтобы создать ваш первый проект, нужно сделать следующее:

new project → scala → sbt

В меню создания проекта нужно выбрать версию SBT и Scala, последними на момент написания статьи являются 1.2.1 и 2.12.6 соответственно.

После нажатия кнопки создания проекта IDEA отдаст указание о создании проекта SBT (это можно понять по надписи dump project structure from SBT), поэтому структура папок проекта появится не сразу.

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

Нас в основном будут интересовать папка src и её подпапки main/scala и test/scala, а также файл build.sbt. Файл build.sbt описывает всю структуру проекта и используется для определения модулей, подмодулей, управления зависимостями и версиями библиотек. Сюда же стоит дописывать импорты сторонних библиотек.

В папке main/scala нужно создать new scala class и выбрать там object (о том, что это такое и какую роль он играет в Scala, будет сказано ниже)

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

После создания обджекта вы увидите следующий код:

В теле обджекта нужно создать точку входа в программу, а именно метод (в Scala методы объявляются ключевым словом ), автодополнение IDEA предложит вам сгенерировать метод как только вы начнёте набирать . В итоге должен получиться такой код:

Если же вам не нужны параметры командной строки , вы можете сократить свой код до:

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

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

Мы настоятельно рекомендуем всем любителям чистых текстовых редакторов на этапе знакомства с языком пользоваться IDEA (пусть это и противоречит философии Unix). Scala содержит большое количество достаточно хитрых конструкций, поддержку которых редко когда можно встретить в редакторе. Кроме того, у IDEA есть много полезных языко-специфических функций. С их помощью IDEA может спасти вас от некоторых глупых ошибок ещё во время написания кода.

Многопозиционная композиция

Пока что всё было легко и понятно. Но на этом мы не остановимся.

Допустим, мы хотим отсортировать массив пар:

Для второго элемента пары не задано отношение порядка (что понятно из его названия), поэтому сравнивать по нему не имеет смысла, более того, такое сравнение просто не скомпилируется. Поэтому сортировку запишем так:

Выглядит длинновато. Можно для красоты отформатировать.

На мой взгляд, это более удобно для чтения, но всё равно достаточно многословно.

Можно ли упростить это выражение? Попробуем написать псевдокод, в котором словами выразим то, что мы хотим сделать:

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

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

Для собственно сравнения в СБШ уже есть подходящий инструмент: .

Но как скомпоновать эти функции вместе? Попробую наглядно проиллюстрировать проблему.

Сравниватель алгоритма ожидает на вход два объекта.

И сам по себе удовлетворяет этому условию.

В то же время, — одноместная функция. Он принимает только один аргумент. Не стыкуется:

Значит, мы не можем просто написать .

При этом результат, который мы хотим получить, должен выглядеть как-то так:

То есть компоновать с функцией мы должны весь этот блок:

Если переводить мысли в код, то мы хотим получить следующую запись:

Но получается, что функция, порождённая записью должна не только принимать, но и возвращать сразу два значения, чтобы подать их на вход . А в языке C++ функция не может вернуть более одного значения!

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

Сам функциональный объект тривиален:

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

А вот и сами механизмы композиции:

Главное отличие от обычной композиции — в раскрытии списка аргументов.
В функции раскрытие происходит так: , а в — иначе: .

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

Теперь мы можем компоновать многоместные функции с одноместными:

Это победа.

На практике: изменяемость в JavaScript

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

Когда мы говорим о функциональном программировании в JS, слово «неизменяемое» используется много, но разработчик обязан всегда держать ее в голове. Например, Redux полагается на одно неизменяемое дерево состояний. Однако сам JavaScript способен изменять объект состояния. Чтобы реализовать неизменяемое дерево состояний, нам нужно каждый раз при изменении состояния возвращать новый объект состояния.

Для неизменяемости объекты JavaScript также могут быть заморожены с помощью

Обратите внимание, что это «неглубокая» заморозка — значения объектов внутри замороженного объекта все еще могут быть изменены. Для гарантированной неизменяемости такие функции «глубокой» заморозки, как Mozilla deepFreeze() и npm deep-freeze могут рекурсивно замораживать объекты

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

Существуют также библиотеки, поддерживающие неизменяемость в JS. Mori предоставляет постоянные структуры данных на основе Clojure. Immutable.js от Facebook также предоставляет неизменяемые коллекции для JS. Библиотеки утилит, такие как Underscore.js и lodash, предоставляют методы и модули для более функционального стиля программирования (а стало быть направленного на неизменяемость).

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

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

Наконец, рассмотрим эту цитату из первого издания Eloquent JavaScript: «Fu-Tzu написал небольшую программу, использующую глобальное состояние и сомнительные переплетения, и, прочитав ее, студент спросил:« Вы предупреждали нас против этих методов, но я нахожу их в вашей программе. Как такое могло случиться?». Фу-Цзы ответил: «Нет необходимости забирать водяной шланг, когда дом не горит». .

Дополнительную информацию о функциональном реактивном программировании можно найти на следующих ресурсах:

  • Функциональное реактивное программирование для начинающих
  • Функциональное реактивное заблуждение
  • Haskell — функциональное реактивное программирование
  • Создание реактивной анимации
  • Более элегантная спецификация для FRP
  • Elm — прощание с FRP
  • Ранние успехи и новые направления в функциональном реактивном программировании
  • Разрушение FRP
  • Rx не является FRP

Заключение

Мы закончим еще одной отличной цитатой из первого издания Eloquent JavaScript: «Студент долгое время сидел за своим компьютером, мрачно хмурился и пытался написать красивое решение сложной проблемы, но не мог найти правильный подход. Фу-Цу ударил его по затылку и крикнул: ‘Введите что-нибудь!’. Студент начал писать уродливое решение, и после того, как он закончил, он внезапно понял прекрасное решение».

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

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

Гость форума
От: admin

Эта тема закрыта для публикации ответов.