- PVSM.RU - https://www.pvsm.ru -

Почему разработчики влюбляются в функциональное программирование?

Функциональное программирование (ФП) существует уже лет 60 [1], но до сих пор оно всегда имело достаточно узкую сферу использования. Хотя компании, меняющие мир, вроде Google, полагаются на его ключевые концепции [2], средний современный программист знает об этом феномене очень мало, если вообще что-то знает.

Но это скоро изменится. В такие языки, как Java [3] и Python [4], интегрируется всё больше и больше концепций ФП. А более современные языки, вроде Haskell, являются полностью функциональными.

Почему разработчики влюбляются в функциональное программирование? - 1 [5]

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

ФП, по своей природе, подходит для решения актуальных задач, вроде задач анализа данных [7] и машинного обучения [8]. Это не означает, что нужно попрощаться с объектно-ориентированным программированием и полностью перейти на функциональное. Современному программисту просто полезно знать основные принципы ФП, что даст ему возможность применить эти принципы там, где они могут сослужить ему хорошую службу.

Суть функционального программирования — это уничтожение побочных эффектов

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

Функция, говоря упрощённо, это сущность, которая преобразует некие входные данные, передаваемые ей, в выходные данные, которые она возвращает в место вызова. Правда, на самом деле всё далеко не всегда выглядит так просто. Взгляните на следующую функцию, написанную на 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]. Это позволяет говорить о том, что список, в действительности, является значением, подаваемым на вход функции, хотя это и никак не зафиксировано при объявлении функции. Это может стать проблемой.

Нечестность при объявлении функций

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

Подумайте о том, как бы вы тестировали функцию 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], да и всё остальное выглядит так же, как прежде.

Но мы, однако, внесли в этот код одно существенное изменение. Мы избавились от побочных эффектов. И это очень хорошо.

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

Функциональное программирование — это написание чистых функций

Функция, при объявлении которой чётко указано то, что она принимает, и то, что она возвращает — это функция без побочных эффектов. Функция без побочных эффектов — это чистая функция.

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

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

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

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

▍Функции 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 [11]»: «Явное лучше, чем неявное».

▍Clojure

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

▍Haskell

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

Итоги

Надо отметить, что сейчас — всё ещё самое начало эры больших данных. Большие данные идут, и не одни, а с другом — с функциональным программированием.

Функциональное программирование, если сравнить его с объектно-ориентированным программированием, всё ещё остаётся нишевым феноменом. Правда, если считать значимым явлением интеграцию принципов ФП в Python и в другие языки, то можно сделать вывод о том, что функциональное программирование набирает популярность.

И в этом есть смысл, так как функциональное программирование хорошо показывает себя в работе с базами данных, в параллельном программировании, в сфере машинного обучения. А в последнее десятилетие всё это находится на подъёме.

Хотя у объектно-ориентированного кода есть бесчисленное множество достоинств, не стоит сбрасывать со счетов и достоинства функционального кода. Если программист изучит некоторые базовые принципы ФП, то этого, в большинстве случаев, может быть достаточно для повышения его профессионального уровня. Такие знания, кроме того, помогут ему подготовиться к «функциональному будущему».

Как вы относитесь к функциональному программированию?

Автор: ru_vds

Источник [12]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka/356231

Ссылки в тексте:

[1] уже лет 60: https://www.cs.kent.ac.uk/people/staff/dat/tfp12/tfp12.pdf

[2] ключевые концепции: https://www.joelonsoftware.com/2006/08/01/can-your-programming-language-do-this/

[3] Java: http://tutorials.jenkov.com/java-functional-programming/index.html

[4] Python: https://docs.python.org/3/howto/functional.html

[5] Image: https://habr.com/ru/company/ruvds/blog/515684/

[6] простыми словами: https://stackoverflow.com/questions/2078978/functional-programming-vs-object-oriented-programming

[7] анализа данных: https://www.haskell.org/communities/05-2018/html/report.html

[8] машинного обучения: https://towardsdatascience.com/functional-programming-for-deep-learning-bc7b80e347e9

[9] реальных сложностей: http://blog.jenkster.com/2015/12/what-is-functional-programming.html

[10] здесь: https://stackoverflow.com/questions/44036657/side-effects-in-python

[11] Дзена Python: https://www.python.org/dev/peps/pep-0020/

[12] Источник: https://habr.com/ru/post/515684/?utm_source=habrahabr&utm_medium=rss&utm_campaign=515684