Введение

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

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

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

  • Функциональное программирование - это не язык. Существует распространенное заблуждение, что существуют «языки функционального программирования», такие как Elixir, Haskell и Scala, и что концепции функционального программирования полезны только на этих языках. Хотя эти языки действительно поощряют написание функциональных программ (и в некоторых случаях обеспечивают соблюдение правил), функциональное программирование не является уникальным для них, и эти методы полезны, независимо от того, используете ли вы пишем ML или PHP.
  • Функциональное программирование - это не библиотека. Это поворот в последнем пункте, так как библиотеки, такие как Vavr для Java и ReactiveX для JavaScript, утверждают, что являются универсальным магазином для функционального программирования, легко подумать, что принципы могут не применяться, если вы их не используете или не можете '' не использовать их.
  • Функциональное программирование предназначено не только для ученых. Хотя корни функционального программирования уходят глубоко в математику, эти принципы может применить любой инженер и улучшить качество и предсказуемость кода этого инженера.

Чтобы прояснить, все это плохо - если вы можете написать Haskell или использовать Vavr в своих Java-проектах, или имеете опыт работы в математике, это замечательно, но они не являются необходимыми для применения принципов функционального программирования. в вашем повседневном коде. Да, даже если ваш работодатель настаивает на использовании Java 5, PHP 5 или Perl 5. Почему всегда версия 5?

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

Побочные эффекты и чистые функции

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

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

https://gist.github.com/m-doughty/1a22ce833f707303d84c4c630044c425

Функцию «тройной» намного проще провести модульное тестирование, потому что ее результат зависит исключительно от вводимых данных. Также намного проще работать параллельно, потому что он не зависит от доступа к общему состоянию. Что наиболее важно, это предсказуемо: triple (5) всегда будет возвращать 15, независимо от того, сколько раз мы запускаем его или что говорит состояние окружающей системы.

Но в реальном мире нам нужен ввод-вывод

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

В мире Java это уже считается лучшей практикой. Алистер Кокберн объясняет в своем введении в гексагональную архитектуру, что бизнес-логика не должна иметь дело с вводом-выводом, и они должны храниться на периферии программного обеспечения и реализовываться с интерфейсами, чтобы упростить их тестирование. Чистая архитектура дяди Боба Мартина - это, по сути, руководство по сохранению побочных эффектов на краю дизайна вашей системы.

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

Ссылочная прозрачность и преобразование данных

«Ссылочная прозрачность» - это причудливый способ сказать, что вы можете заменить функцию ее результатом и наоборот, не вызывая побочных эффектов. (2 + 2) * 4 === 4 * 4.

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

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

Когда-то являвшиеся прерогативой нишевых академических языков, неизменные ценности теперь проникли в большинство основных языков. Будь то const в ES6, последнее ключевое слово в Java или val / var в Kotlin, большинство языков приняли будущее, в котором мы не изменяем структуры данных, а копируем их.

Это может показаться нелогичным: зачем нам что-то копировать, а не изменять? Ответ кроется в непредвиденных эффектах:

https://gist.github.com/m-doughty/ee1ec19e7ba313ed886de93d4c4db9cf

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

На самом деле Haskell идет еще дальше и не позволяет переназначать значения. Если вы говорите, что x равен 3, то x равен 3 для остальной части лексической области видимости, и нет никакого способа указать программе иначе.

Рекурсия

В императивном и объектно-ориентированном программировании часто используются циклы (for, while, forEach), которые имеют побочные эффекты (увеличение итератора, изменение переменной 'while' или изменение структуры, по которой выполняется итерация). В функциональном программировании мы предпочитаем рекурсию. Рекурсивная функция - это функция, которая обращается к самой себе с измененными входными значениями (в противном случае у нас был бы бесконечный цикл, который предсказуем, но не очень полезен) до тех пор, пока он не «закончит» обработку.

Наиболее распространенные рекурсивные функции для замены циклов (все из которых уже реализованы на большинстве языков):

  • map, который принимает входную коллекцию и применяет функцию к каждому элементу в коллекции, а затем возвращает выходную коллекцию равного размера (примечание: входная коллекция является неизменной, поэтому не будет изменяться в процессе);
  • фильтр, который принимает набор входных данных и предикат (функцию, которая возвращает логическое значение) и проверяет каждый вход на соответствие предикату, а затем возвращает только набор элементов, которые возвращают значение true; и
  • уменьшить (или свернуть), который принимает входную коллекцию и применяет функцию, которая принимает аккумулятор и значение и превращает его в одно значение, в конечном итоге возвращая одно значение ( например String или Int).

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

Вот несколько примеров использования рекурсии для расчета бонусов сотрудникам:

https://gist.github.com/m-doughty/9eeca6552b9657fd2a5ff089d2ba1089

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

Контекстные «коробки»

Один из инструментов, который любят использовать функциональные программисты и который реализован на многих языках, - это перенос контекста. Это когда мы берем значение (или функцию) и помещаем его в рамку. Возможно, вам знакомо Возможно / Вариант / Необязательно. Maybe Int может иметь один из двух результатов в зависимости от контекста; это может быть Just Int (т. е. Just 5) или Nothing.

Используемый язык может различаться - Just / Some или Nil / None / Nothing - но идея обертывания контекста состоит в том, что мы можем «упаковать» два или более типа результата в одно значение и только распаковывать их только тогда, когда нам нужно.

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

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

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

Ценность контекста двоякая:

  • Помещая логику о том, как обрабатывать нулевые значения, ошибки и другие обстоятельства, не относящиеся к золотому пути, в поля, мы избавляем себя от необходимости думать о них во всех функциях, которые их обрабатывают - не более if (x = == null) || (x === «») защитные предложения, и больше никаких «вы забыли проверить нулевой регистр» в запросах на вытягивание;
  • Мы можем передать контекст полностью через нашу «сантехнику» преобразования данных и обработать крайние случаи по краям. Вместо того, чтобы определять, что делать, когда генерируется исключение или вычисление возвращает ноль, и перенаправлять нашу логику на лету, мы можем вывести ошибку на край и обработать ее там, сохраняя как можно больше чистого кода. .

Резюме

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

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

  • Легко сочинять;
  • Легко протестировать;
  • Легко рассуждать;
  • Легко предсказать.

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