- Почему разработчики влюбляются в функциональное программирование?
- Суть функционального программирования — это уничтожение побочных эффектов
- Нечестность при объявлении функций
- Функциональное программирование — это написание чистых функций
- Чем не является функциональное программирование
- ▍Функции map и reduce
- ▍Лямбда-функции
- ▍Статическая типизация
- Некоторые языки «функциональнее» других
- ▍Perl
- ▍Java
- ▍Scala
- ▍Python
- ▍Clojure
- ▍Haskell
- Итоги
Почему разработчики влюбляются в функциональное программирование?
Функциональное программирование (ФП) существует уже лет 60, но до сих пор оно всегда имело достаточно узкую сферу использования. Хотя компании, меняющие мир, вроде Google, полагаются на его ключевые концепции, средний современный программист знает об этом феномене очень мало, если вообще что-то знает.
Но это скоро изменится. В такие языки, как Java и Python, интегрируется всё больше и больше концепций ФП. А более современные языки, вроде Haskell, являются полностью функциональными.
Если описать функциональное программирование простыми словами, то это — создание функций для работы с неизменяемыми переменными. В противоположность этому, объектно-ориентированное программирование — это когда используется сравнительно постоянный набор функций, а программист, в основном, занят модификацией существующих переменных и созданием новых.
ФП, по своей природе, подходит для решения актуальных задач, вроде задач анализа данных и машинного обучения. Это не означает, что нужно попрощаться с объектно-ориентированным программированием и полностью перейти на функциональное. Современному программисту просто полезно знать основные принципы ФП, что даст ему возможность применить эти принципы там, где они могут сослужить ему хорошую службу.
Суть функционального программирования — это уничтожение побочных эффектов
Для того чтобы понять принципы функционального программирования, сначала надо разобраться в том, что такое «функция». Может, это покажется скучным, но, в итоге, это позволит увидеть то, что на первый взгляд незаметно. Поэтому давайте поговорим о функциях.
Функция, говоря упрощённо, это сущность, которая преобразует некие входные данные, передаваемые ей, в выходные данные, которые она возвращает в место вызова. Правда, на самом деле всё далеко не всегда выглядит так просто. Взгляните на следующую функцию, написанную на Python:
Эта функция крайне проста. Она принимает один аргумент, x , который, вероятно, имеет тип int , а, может быть, тип float или double , и выдаёт результат возведения этого x в квадрат.
global_list = [] def append_to_list(x): global_list.append(x)
На первый взгляд кажется, что она принимает x какого-то типа и ничего не возвращает, так как в ней нет выражения return . Но не будем спешить с выводами!
Функция не сможет нормально работать в том случае, если заранее не будет объявлена переменная global_list . Результатом работы этой функции является модифицированный список, хранящийся в global_list . Даже хотя global_list не объявлен в качестве значения, которое подаётся на вход функции, данная переменная меняется после вызова функции.
append_to_list(1) append_to_list(2) global_list
После пары вызовов функции из предыдущего примера в global_list будет уже не пустой список, а список [1,2] . Это позволяет говорить о том, что список, в действительности, является значением, подаваемым на вход функции, хотя это и никак не зафиксировано при объявлении функции. Это может стать проблемой.
Нечестность при объявлении функций
Эти неявные входные или выходные значения имеют официальное наименование: побочные эффекты. Тут мы используем очень простые примеры, но в более сложных программах побочные эффекты способны приводить к возникновению реальных сложностей.
Подумайте о том, как бы вы тестировали функцию append_to_list . Недостаточно будет прочесть первую строку её объявления, и выяснить, что её надо тестировать, передавая ей некое значение x . Вместо этого нужно будет читать весь код функции, разбираться в том, что именно там происходит, объявлять переменную global_list , а потом уже тестировать функцию. То, что в нашем простом примере, как кажется, особых сложностей не вызывает, в программах, состоящих из тысяч строк кода, будет выглядеть уже совсем по-другому.
К счастью, вышеозначенную проблему легко исправить. Нужно просто быть честным при указании того, что именно должно поступать на вход функции. Следующий вариант нашей функции выглядит уже куда лучше предыдущего:
newlist = [] def append_to_list2(x, some_list): some_list.append(x) append_to_list2(1,newlist) append_to_list2(2,newlist) newlist
Мы не особенно многое изменили в этом коде. В результате работы функции в newlist , как раньше в global_list , оказывается [1,2] , да и всё остальное выглядит так же, как прежде.
Но мы, однако, внесли в этот код одно существенное изменение. Мы избавились от побочных эффектов. И это очень хорошо.
А именно, теперь, прочтя первую строку объявления функции, мы точно знаем о том, с какими входными данными она работает. В результате, если программа не будет вести себя так, как ожидается, можно легко протестировать каждую имеющуюся в ней функцию и найти ту, которая работает неправильно. Чистые функции легче поддерживать.
Функциональное программирование — это написание чистых функций
Функция, при объявлении которой чётко указано то, что она принимает, и то, что она возвращает — это функция без побочных эффектов. Функция без побочных эффектов — это чистая функция.
Вот очень простое определение функционального программирования. Это — написание программ, состоящих только из чистых функций. Чистые функции никогда не модифицирую переданные им данные, они лишь создают новые и возвращают их. (Отмечу, что я немного смошенничал в предыдущем примере. Он написан в духе функционального программирования, но в нём функция модифицирует глобальную переменную-список. Но здесь мы лишь разбираем базовые принципы ФП, поэтому я и поступил именно так. Если хотите, здесь вы можете найти более строгие примеры чистых функций.)
Далее, работая с чистыми функциями, можно ожидать того, что они, получая на вход одни и те же данные, всегда будут формировать одни и те же выходные данные. А функции, которые чистыми не являются, могут зависеть от каких-то глобальных переменных. В результате они, получая одно и то же на вход, могут выдавать разные результаты, зависящие от значения глобальных переменных. Этот факт способен значительно усложнить отладку и поддержку кода.
Есть простое правило, которое позволяет обнаруживать побочные эффекты. Так как при объявлении чистых функций должно быть чётко определено то, что они получают на вход и возвращают, функции, которые ничего не принимают и не возвращают, чистыми не будут. Если вы решите внедрить в свой проект методы функционального программирования, то первым делом вы, вероятно, решите проверить объявления своих функций.
Чем не является функциональное программирование
▍Функции map и reduce
Циклы — это механизмы, не имеющие отношения к функциональному программированию. Взгляните на следующие Python-циклы:
integers = [1,2,3,4,5,6] odd_ints = [] squared_odds = [] total = 0 for i in integers: if i%2 ==1 odd_ints.append(i) for i in odd_ints: squared_odds.append(i*i) for i in squared_odds: total += i
С помощью этого кода мы решаем простые задачи, но получился он довольно длинным. И он, кроме того, не является функциональным, так как тут производится модификация глобальных переменных.
А теперь — ещё один вариант этого кода:
from functools import reduce integers = [1,2,3,4,5,6] odd_ints = filter(lambda n: n % 2 == 1, integers) squared_odds = map(lambda n: n * n, odd_ints) total = reduce(lambda acc, n: acc + n, squared_odds)
Это — полностью функциональный код. Он короче. Он быстрее, так как тут не приходится перебирать множество элементов массива. И, если разобраться с функциями filter , map и reduce , окажется, что этот код понять не намного сложнее, чем тот, в котором применяются циклы.
Это не значит, что в любом функциональном коде используются map , reduce и прочие подобные функции. И это не означает, что для того чтобы с подобными функциями разобраться, нужно знать функциональное программирование. Дело лишь в том, что эти функции достаточно часто применяются тогда, когда избавляются от циклов.
▍Лямбда-функции
Когда говорят об истории функционального программирования, часто начинают с рассказа об изобретении лямбда-функций. Но, хотя лямбда-функции — это, без сомнения, краеугольный камень функционального программирования, они не являются главной причиной возникновения ФП.
Лямбда-функции — это инструменты, которые можно использовать для того чтобы писать программы в функциональном стиле. Но эти функции можно использовать и в объектно-ориентированном программировании.
▍Статическая типизация
Вышеприведённый пример не типизирован статически. Но он, тем не менее, представляет собой образец функционального кода.
Даже хотя статическая типизация добавляет дополнительный слой безопасности в код, она не является обязательным условием создания функционального кода. Она, правда, может быть приятным дополнением к функциональному стилю программирования.
Надо отметить, что на некоторых языках программировать в функциональном стиле легче, чем на других.
Некоторые языки «функциональнее» других
▍Perl
В Perl реализован такой подход к работе с побочными эффектами, который отличает его от большинства других языков. А именно, в нём имеется «волшебная переменная» $_ , которая выводит побочные эффект на уровень одной из основных возможностей языка. У Perl есть свои достоинства, но я не стал бы пытаться заниматься функциональным программированием на этом языке.
▍Java
Желаю вам удачи в деле написания функционального кода на Java. Она вам не помешает. Во-первых, половину объёма кода будет занимать ключевое слово static . Во-вторых, большинство Java-программистов назовут ваш код недоразумением.
Это не значит, что Java — плохой язык. Но он не создан для решения тех задач, для решения которых отлично подходит функциональное программирование. Например — для управления базами данных или для разработки приложений из сферы машинного обучения.
▍Scala
Scala — интересный язык. Его цель — унификация функционального и объектно-ориентированного программирования. Если вам это кажется странным, то знайте, что вы не одиноки. Ведь функциональное программирование нацелено на полное устранение побочных эффектов. А объектно-ориентированное программирование направлено на ограничение побочных эффектов рамками объектов.
Учитывая это, можно сказать, что многие разработчики видят в Scala язык, который поможет им перейти от объектно-ориентированного к функциональному программированию. Использование Scala может упростить для них, в будущем, переход на полностью функциональный стиль программирования.
▍Python
В Python приветствуется функциональный стиль программирования. Понять это можно, если учесть тот факт, что у каждой функции, по умолчанию, есть, как минимум, один параметр — self . Это, во многом, в духе «Дзена Python»: «Явное лучше, чем неявное».
▍Clojure
Clojure, по словам создателя языка, является функциональным примерно на 80%. Все значения, по умолчанию, неизменяемы. А ведь именно это и нужно для написания функционального кода. Правда, обойти это можно, используя изменяемые контейнеры, в которые помещают неизменяемые значения. А если извлечь значение из контейнера — оно снова становится неизменяемым.
▍Haskell
Это — один из немногих полностью функциональных и статически типизированных языков. Хотя при его использовании в процессе разработки и может показаться, что на реализацию функциональных механизмов уходит слишком много времени, подобные усилия многократно окупятся во время отладки кода. Этот язык выучить не так просто, как другие, но его изучение — это, безусловно, стоящее вложение времени.
Итоги
Надо отметить, что сейчас — всё ещё самое начало эры больших данных. Большие данные идут, и не одни, а с другом — с функциональным программированием.
Функциональное программирование, если сравнить его с объектно-ориентированным программированием, всё ещё остаётся нишевым феноменом. Правда, если считать значимым явлением интеграцию принципов ФП в Python и в другие языки, то можно сделать вывод о том, что функциональное программирование набирает популярность.
И в этом есть смысл, так как функциональное программирование хорошо показывает себя в работе с базами данных, в параллельном программировании, в сфере машинного обучения. А в последнее десятилетие всё это находится на подъёме.
Хотя у объектно-ориентированного кода есть бесчисленное множество достоинств, не стоит сбрасывать со счетов и достоинства функционального кода. Если программист изучит некоторые базовые принципы ФП, то этого, в большинстве случаев, может быть достаточно для повышения его профессионального уровня. Такие знания, кроме того, помогут ему подготовиться к «функциональному будущему».
Как вы относитесь к функциональному программированию?