Но это скоро изменится. В такие языки, как Java и Python, интегрируется всё больше и больше концепций ФП. А более современные языки, вроде Haskell, являются полностью функциональными.
Если описать функциональное программирование простыми словами, то это — создание функций для работы с неизменяемыми переменными. В противоположность этому, объектно-ориентированное программирование — это когда используется сравнительно постоянный набор функций, а программист, в основном, занят модификацией существующих переменных и созданием новых.
ФП, по своей природе, подходит для решения актуальных задач, вроде задач анализа данных и машинного обучения. Это не означает, что нужно попрощаться с объектно-ориентированным программированием и полностью перейти на функциональное. Современному программисту просто полезно знать основные принципы ФП, что даст ему возможность применить эти принципы там, где они могут сослужить ему хорошую службу.
Суть функционального программирования — это уничтожение побочных эффектов
Для того чтобы понять принципы функционального программирования, сначала надо разобраться в том, что такое «функция». Может, это покажется скучным, но, в итоге, это позволит увидеть то, что на первый взгляд незаметно. Поэтому давайте поговорим о функциях.
Функция, говоря упрощённо, это сущность, которая преобразует некие входные данные, передаваемые ей, в выходные данные, которые она возвращает в место вызова. Правда, на самом деле всё далеко не всегда выглядит так просто. Взгляните на следующую функцию, написанную на Python:
def square(x):
return x*x
Эта функция крайне проста. Она принимает один аргумент,
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 и в другие языки, то можно сделать вывод о том, что функциональное программирование набирает популярность.
И в этом есть смысл, так как функциональное программирование хорошо показывает себя в работе с базами данных, в параллельном программировании, в сфере машинного обучения. А в последнее десятилетие всё это находится на подъёме.
Хотя у объектно-ориентированного кода есть бесчисленное множество достоинств, не стоит сбрасывать со счетов и достоинства функционального кода. Если программист изучит некоторые базовые принципы ФП, то этого, в большинстве случаев, может быть достаточно для повышения его профессионального уровня. Такие знания, кроме того, помогут ему подготовиться к «функциональному будущему».
Как вы относитесь к функциональному программированию?
Комментариев нет:
Отправить комментарий