Чистые функции — Python: Функции
Часто в программировании возникает проблема с неявными зависимостями и побочными эффектами. Они могут привести к неожиданным результатам или ошибкам в программе. Обычно с такими проблемами борются с помощью чистых функций.
В этом уроке мы изучим важные свойства функций. С их помощью можно более точно определять, как лучше разбивать код на функции и когда вообще их стоит выделять.
Что такое детерминированность
Детерминированность функций — это свойство, при котором функция всегда возвращает один и тот же результат для одних и тех же входных данных. Это происходит без побочных эффектов на состояние программы. Детерминированная функция не зависит от контекста и внешних факторов. Она гарантированно возвращает предсказуемый результат.
Рассмотрим пример функции, которая не является детерминированной:
Эта функция возвращает случайное число от 1 до 10 при каждом вызове. Если мы вызываем функцию несколько раз, то получим разные результаты:
Теперь рассмотрим пример детерминированной функции:
Функция multiply всегда возвращает результат умножения двух переданных ей аргументов. Если мы передадим одни и те же значения, то получим один и тот же результат:
Это делает функцию multiply детерминированной, так как она всегда возвращает один и тот же результат для одного и того же набора входных данных.
Нельзя однозначно сказать, что отсутствие детерминированности — это плохо. Для работы многих программ и сайтов нужна функция, которая будет возвращать случайное число или вычислять текущую дату. Но мы можем разделить код так, чтобы в нем было как можно больше детерминированных частей.
Если есть возможность написать функцию так, что она будет детерминированной, то так и нужно делать. При этом не стоит использовать глобальные переменные. Необходимо создавать функции, которые зависят только от своих аргументов.
Что такое побочные эффекты
Побочный эффект или side effects — это любые взаимодействия с внешней средой. Они включают в себя изменения глобальных переменных и операции с файлами. Например, запись в файл, чтение из файла, отправка или прием данных по сети и вывод в консоль.
Рассмотрим пример функции, которая имеет побочный эффект:
Эта функция выводит сообщение в консоль, значит, она имеет побочный эффект. Если мы вызываем функцию print_hello , то она изменяет состояние программы путем вывода сообщения в консоль. Но она не возвращает никакого значения:
Побочные эффекты составляют одну из самых больших сложностей при разработке. Они затрудняют логику кода и тестирование. Это приводит к большому числу ошибок.
Например, только при работе с файлами количество возможных ошибок измеряется сотней. В этом случае может не хватить места на диске или будет попытка прочитать данные из несуществующего файла. Для предотвращения ошибок код обрастает большим числом проверок и защитных механизмов.
Без побочных эффектов невозможно написать ни одной полезной программы. Результат любых вычислений должен быть продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит к побочным эффектам.
В реальных приложениях побочные эффекты могут быть полезны. Например, когда нужно записать данные в файл или отправить запрос на сервер. Но в программировании рекомендуется минимизировать количество функций с побочными эффектами. Это сделает программу более предсказуемой и легко тестируемой.
Что такое чистые функции
Чистые функции или pure functions — это функции, которые при вызове не влияют на состояние программы и не имеют побочных эффектов. Они возвращают значения только на основе входных аргументов и не изменяют их.
Признаки чистых функций:
- Всегда возвращают одинаковые значения для одинаковых аргументов
- Не имеют побочных эффектов — не изменяют состояние программы за пределами своей области видимости. Таким образом они не модифицируют глобальные переменные, не изменяют содержимое файлов, не выводят на экран и не изменяют содержимое баз данных
- Не зависят от состояния программы, поэтому не используют глобальные переменные или переменные, которые могут изменяться во время выполнения программы
- Не имеют побочных эффектов на аргументы, которые переданы им по ссылке — не изменяет их содержимое
Примеры чистых функций в Python:
Эти функции возвращают значение только на основе переданных им аргументов и не влияют на другие переменные в программе.
У чистых функций есть несколько преимуществ:
- Проще для тестирования. Результаты их работы можно легко предсказать и проверить
- Более безопасны. Поскольку они не изменяют состояние программы, то не могут вызвать неожиданные побочные эффекты или ошибки в других частях программы
- Легче поддаются оптимизации. Поскольку они не имеют побочных эффектов, их можно безопасно кэшировать или выполнять в многопоточной среде
Что такое грязные функции
Не все функции могут быть чистыми, особенно, когда программы часто взаимодействуют с внешними ресурсами. В таких случаях функции могут изменять состояние программы или взаимодействовать с внешними системами, такими как базы данных или веб-сервисы. Такие функции называют грязными.
Рассмотрим несколько примеров.
Функции, которые изменяют глобальные переменные
В этом примере функция increment() увеличивает значение глобальной переменной count на единицу при каждом вызове. Это может привести к неожиданным результатам в других частях программы.
Функции, которые изменяют аргументы
В примере функция append_list() добавляет элемент в конец переданного ей списка my_list . Это часто приводит к ошибкам в расчетах в других частях программы.
Функции, которые работают с файлами
В приведенном примере функция write_file() записывает строку в файл test.txt . Это может иметь побочные эффекты, такие как изменение содержимого файла или его положения в файле.
Функции, которые имеют побочный эффект вывода на экран
В данном примере функция print_and_return() выводит переданный ей аргумент и возвращает его. Это также может привести к неожиданным результатам в виде излишнего вывода на экран.
Выводы
Чистые и грязные функции имеют разные особенности и применения.
Чистые функции являются более предпочтительными. Они проще для понимания и тестирования, а также могут быть использованы в многопоточных приложениях без каких-либо проблем.
Грязные функции могут быть полезными в некоторых случаях, например, когда нужно изменить состояние программы или выполнить ввод-вывод. Но такие функции могут усложнить отладку и тестирование приложения.
Рекомендуем использовать чистые функции, когда это возможно. А использование грязных функций лучше ограничить. Их стоит применять только для тех случаев, когда это действительно необходимо.
Как сделать функции на Python еще лучше
Собственно, заголовок этой замечательной статьи от Джеффа Кнаппа (Jeff Knupp), автора книги «Writing Idiomatic Python» полностью отражает ее суть. Читайте внимательно и не стесняйтесь комментировать.
Поскольку очень не хотелось оставлять в тексте важный термин латиницей, мы позволили себе перевести слово «docstring» как «докстрока», обнаружив этот термин в нескольких русскоязычных источниках.
В Python, как и в большинстве современных языков программирования, функция – это основной метод абстрагирования и инкапсуляции. Вы, будучи разработчиком, вероятно, написали уже сотни функций. Но функции функциям – рознь. Причем, если писать «плохие» функции, это немедленно скажется на удобочитаемости и поддержке вашего кода. Итак, что же такое «плохая» функция, а еще важнее – как сделать из нее «хорошую»?
Освежим тему
Математика изобилует функциями, правда, припомнить их сложно. Так что давайте вернемся к нашей излюбленной дисциплине: анализу. Вероятно, вам доводилось видеть формулы вроде f(x) = 2x + 3 . Это функция под названием f , принимающая аргумент x , а затем «возвращающая» дважды x + 3 . Хотя, она и не слишком похожа на те функции, к которым мы привыкли в Python, она совершенно аналогична следующему коду:
Функции издавна существуют в математике, но в информатике совершенно преображаются. Однако, эта сила не дается даром: приходится миновать различные подводные камни. Давайте же обсудим, какова должна быть «хорошая» функция, и какие «звоночки» характерны для функций, возможно, требующих рефакторинга.
Секреты хорошей функции
Что отличает «хорошую» функцию Python от посредственной? Вы удивитесь, как много трактовок допускает слово «хорошая». В рамках этой статьи я буду считать функцию Python «хорошей», если она удовлетворяет большинству пунктов из следующего списка (выполнить все пункты для конкретной функции порой невозможно):
- Она внятно названа
- Соответствует принципу единственной обязанности
- Содержит докстроку
- Возвращает значение
- Состоит не более чем из 50 строк
- Она идемпотентная и, если это возможно, чистая
Вот моя любимая цитата на эту тему, часто ошибочно приписываемая Дональду, а на самом деле принадлежащая Филу Карлтону:
Как бы глупо это ни звучало, именование – действительно сложная штука. Вот пример «плохого» названия функции:
Теперь плохие названия попадаются мне практически повсюду, но данный пример взят из области Data Science (точнее, машинного обучения), где практикующие специалисты обычно пишут код в блокноте Jupyter, а потом пытаются собрать из этих ячеек удобоваримую программу.
Первая проблема с названием этой функции – в нем используются аббревиатуры. Лучше использовать полные английские слова, а не аббревиатуры и не малоизвестные сокращения. Единственная причина, по которой хочется сокращать слова — не тратить сил на набор лишнего текста, но в любом современном редакторе есть функция автозавершения, поэтому вам придется набрать полное название функции всего один раз. Аббревиатура – это проблема, поскольку зачастую она специфична для предметной области. В вышеприведенном коде knn означает «K-ближайшие соседи», а df означает «DataFrame», структуру данных, повсеместно используемую в библиотеке pandas. Если код будет читать программист, не знающий этих сокращений, то он практически ничего не поймет в названии функции.
Еще в названии этой функции есть два более мелких недочета. Во-первых, слово «get» избыточно. В большинстве грамотно поименованных функций сразу понятно, что данная функция что-то возвращает, что конкретно – отражено в имени. Элемент from_d f также не нужен. Либо в докстроке функции, либо (если она находится на периферии) в аннотации типа будет описан тип параметра, если эта информация и так не очевидна из названия параметра.
Так как же нам переименовать эту функцию? Просто:
Теперь даже неспециалисту понятно, что вычисляется в этой функции, а имя параметра (dataframe) не оставляет сомнений, какой аргумент ей следует передавать.
Единственная ответственность
Развивая мысль Боба Мартина, скажу, что Принцип единственной ответственности касается функций не меньше, чем классов и модулей (о которых изначально и писал господин Мартин). Согласно этому принципу (в нашем случае) у функции должна быть единственная ответственность. То есть, она должна делать одну и только одну вещь. Один из самых веских доводов в пользу этого: если функция делает всего одну вещь, то и переписывать ее придется в единственном случае: если эту самую вещь придется делать по-новому. Также становится ясно, когда функцию можно удалить; если, внеся изменения где-то в другом месте, мы поймем, что единственная обязанность функции более не актуальна, то мы от нее просто избавимся.
Здесь лучше привести пример. Вот функция, делающая более одной «вещи»:
А именно две: вычисляет набор статистических данных о списке чисел и выводит их в STDOUT . Функция нарушает правило: должна быть единственная конкретная причина, по которой ее, возможно, потребовалось бы изменить. В данном случае просматриваются две очевидные причины, по которым это понадобится: либо потребуется вычислять новую или иную статистику, либо потребуется изменить формат вывода. Поэтому данную функцию лучше переписать в виде двух отдельных функций: одна будет выполнять вычисления и возвращать их результаты, а другая – принимать эти результаты и выводить их в консоль. Функцию (вернее, наличие у нее двух обязанностей) с потрохами выдает слово and в ее названии.
Такое разделение также серьезно упрощает тестирование функции, а еще позволяет не только разбить ее на две функции в рамках одного и того же модуля, но даже разнести две эти функции в совершенно разные модули, если это уместно. Это дополнительно способствует более чистому тестированию и упрощает поддержку кода.
На самом деле, функции, выполняющие ровно две вещи, встречаются редко. Гораздо чаще натыкаешься на функции, делающие намного, намного больше операций. Опять же, из соображений удобочитаемости и тестируемости такие «многостаночные» функции следует дробить на однозадачные, в каждой из которых заключен единственный аспект работы.
Докстроки
Казалось бы, все в курсе, что есть документ PEP-8, где даются рекомендации по стилю кода на Python, но гораздо меньше среди нас тех, кто знает PEP-257, в котором такие же рекомендации даются по поводу докстрок. Чтобы не пересказывать содержание PEP-257, отсылаю вас самих к этому документу – почитайте в свободное время. Однако, основные его идеи таковы:
- Для каждой функции нужна докстрока
- В ней следует соблюдать грамматику и пунктуацию; писать законченными предложениями
- Докстрока начинается с краткого (в одно предложение) описания того, что делает функция
- Докстрока формулируется в предписывающем, а не в описательном стиле
Возвращаемые значения
Функции можно (и следует) трактовать как маленькие самодостаточные программы. Они принимают некоторый ввод в форме параметров и возвращают результат. Параметры, конечно, опциональны. А вот возвращаемые значения обязательны с точки зрения внутреннего устройства Python. Если вы даже попытаетесь написать функцию, которая не возвращает значения – не сможете. Если функция даже не станет возвращать значения, то интерпретатор Python «принудит» ее возвращать None . Не верите? Попробуйте сами:
Как видите, значение b – по сути None . Итак, даже если вы напишете функцию без инструкции return, она все равно будет что-то возвращать. И должна. В конце концов, это ведь маленькая программа, верно? Насколько полезны программы, от которых нет никакого вывода – и поэтому невозможно судить, верно ли выполнилась данная программа? Но самое важное – как вы собираетесь тестировать такую программу?
Я даже не побоюсь утверждать следующее: каждая функция должна возвращать полезное значение, хотя бы ради тестируемости. Код, который я пишу, должен быть протестирован (это не обсуждается). Только представьте, каким корявым может получится тестирование вышеприведенной функции add (подсказка: вам придется перенаправлять ввод/вывод, после чего вскоре все пойдет наперекосяк). Кроме того, возвращая значение, мы можем выполнять сцепление методов и, следовательно, писать код вот так:
Строка if line.strip().lower().endswith(‘cat’): работает, поскольку каждый из строковых методов ( strip() , lower() , endswith() ) в результате вызова функции возвращает строку.
Вот несколько распространенных доводов, которые вам может привести программист, объясняя, почему написанная им функция не возвращает значения:
Этот аргумент немного надуманный, но мне доводилось его слышать. Ответ, разумеется, как раз в том, что автор и хотел сделать – но не знал как: для возврата нескольких значений используйте кортеж.
Наконец, самый сильный аргумент в пользу того, что полезное значение лучше возвращать в любом случае – в том, что вызывающая сторона всегда может с полным правом эти значения игнорировать. Короче говоря, возврат значения от функции – практически наверняка здравая идея, и крайне маловероятно, что мы таким образом что-нибудь повредим, даже в сложившихся базах кода.
Длина функции
Я не раз признавался, что довольно туп. Могу одновременно держать в голове примерно три вещи. Если вы дадите мне прочесть 200-строчную функцию и спросите, что она делает, я, вероятно, буду таращиться на нее не менее 10 секунд. Длина функции прямо сказывается на ее удобочитаемости и, следовательно, на поддержке. Поэтому старайтесь, чтобы ваши функции оставались короткими. 50 строк – величина, взятая совершенно с потолка, но мне она кажется разумной. (Надеюсь), что большинство функций, которые вам доведется писать, будут значительно короче.
Если функция соответствует Принципу единственной ответственности, то, вероятно, она будет достаточно краткой. Если она читая или идемпотентная (об этом мы поговорим) ниже – то, наверное, она также получится короткой. Все эти идеи гармонично сочетаются друг с другом и помогают писать хороший, чистый код.
Итак, что же делать, если ваша функция получилась слишком длинной? РЕФАКТОРИТЬ! Вероятно, вам приходится заниматься рефакторингом постоянно, даже если вы не знаете этого термина. Рефакторинг – это попросту изменение структуры программы, без изменения ее поведения. Поэтому, извлечение нескольких строк кода из длинной функции и превращение их в самостоятельную функцию – это один из типов рефакторинга. Оказывается, это еще и наиболее распространенный, и самый быстрый способ продуктивного укорачивания длинных функций. Поскольку вы даете этим новым функциям подходящие имена, получающийся у вас код гораздо проще читать. Я написал целую книгу о рефакторинге (на самом деле, я им постоянно занимаюсь), так что здесь вдаваться в детали не буду. Просто знайте, что, если у вас есть слишком длинная функция – то ее следует рефакторить.
Идемпотентность и функциональная чистота
Заголовок этого раздела может показаться слегка устрашающим, но концептуально раздел прост. Идемпотентная функция при одинаковом наборе аргументов всегда возвращает одно и то же значение, независимо от того, сколько раз ее вызывают. Результат не зависит от нелокальных переменных, изменяемости аргументов или от любых данных, поступающих из потоков ввода/вывода. Следующая функция add_three(number) идемпотентна:
Независимо от того, сколько раз мы вызовем add_three(7) , ответ всегда будет равен 10. А вот другой случай – функция, не являющаяся идемпотентной:
Эта откровенно надуманная функция не идемпотентна, поскольку возвращаемое значение функции зависит от ввода/вывода, а именно – от числа, введенного пользователем. Разумеется, при разных вызовах add_three() возвращаемые значения будут отличаться. Если мы дважды вызовем эту функцию, то пользователь в первом случае может ввести 3, а во втором – 7, и тогда два вызова add_three() вернут 6 и 10 соответственно.
Вне программирования также встречаются примеры идемпотентности – например, по такому принципу устроена кнопка «вверх» у лифта. Нажимая ее в первый раз, мы «уведомляем» лифт, что хотим подняться. Поскольку кнопка идемпотентна, то сколько ее потом ни нажимать – ничего страшного не произойдет. Результат будет всегда одинаков.
Почему идемпотентность так важна
Тестируемость и удобство в поддержке. Идемпотентные функции легко тестировать, поскольку они гарантированно, в любом случае вернут одинаковый результат, если вызвать их с одними и теми же аргументами. Тестирование сводится к проверке того, что при разнообразных вызовах функция всегда возвращает ожидаемое значение. Более того, эти тесты будут быстрыми: скорость тестов – важная проблема, которую часто обходят вниманием при модульном тестировании. А рефакторинг при работе с идемпотентными функциями – вообще легкая прогулка. Не важно, как вы измените код вне функции – результат ее вызова с одними и теми же аргументами всегда будет один и тот же.
Что такое «чистая» функция?
В функциональном программировании функция считается чистой, если она, во-первых, идемпотентна, а во-вторых – не вызывает наблюдаемых побочных эффектов. Не забывайте: функция идемпотентна, если всегда возвращает один и тот же результат при конкретном наборе аргументов. Однако, это не означает, что функция не может влиять на другие компоненты – например, на нелокальные переменные или потоки ввода/вывода. Например, если бы идемпотентная версия вышеприведенной функции add_three(number) выводила результат в консоль, а лишь затем возвращала бы его, она все равно считалась бы идемпотентной, поскольку при ее обращении к потоку ввода/вывода эта операция доступа никак не влияет на значение, возвращаемое от функции. Вызов print() – это просто побочный эффект: взаимодействие с остальной программой или системой как таковой, происходящее наряду с возвратом значения.
Давайте немного разовьем наш пример с add_three(number) . Можно написать следующий код, чтобы определить, сколько раз была вызвана add_three(number) :
Теперь мы выполняем вывод в консоль (это побочный эффект) и изменяем нелокальную переменную (другой побочный эффект), но, поскольку ни то, ни другое не влияет на значение, возвращаемое функцией, она все равно идемпотентна.
Чистая функция не оказывает побочных эффектов. Она не только не использует никаких «внешних данных» при расчете значения, но и не взаимодействует с остальной программой/системой, только вычисляет и возвращает указанное значение. Следовательно, хотя наше новое определение add_three(number) остается идемпотентным, эта функция уже не чистая.
В чистых функциях нет инструкций логирования или вызовов print() . При работе они не обращаются к базе данных и не используют соединений с интернетом. Не обращаются к нелокальным переменным и не изменяют их. И не вызывают других не-чистых функций.
Короче говоря, они не оказывают «жуткого дальнодействия», выражаясь словами Эйнштейна (но в контексте информатики, а не физики). Они не изменяют каким-либо образом остальные части программы или системы. В императивном программировании (а именно им вы и занимаетесь, когда пишете код на Python), такие функции – самые безопасные. Они известны своей тестируемостью и удобством в поддержке; более того, поскольку они идемпотентны, тестирование таких функций гарантированно будет столь же быстрым, как и выполнение. Сами тесты также просты: не приходится подключаться к базе данных либо имитировать какие-либо внешние ресурсы, готовить стартовую конфигурацию кода, а по окончании работы не нужно ничего подчищать.
Честно говоря, идемпотентность и чистота очень желательны, но не обязательны. То есть, нам бы хотелось писать только чистые или идемпотентные функции, учитывая все вышеупомянутые их преимущества, но это не всегда возможно. Суть, однако, в том, чтобы приучиться писать код, естественным образом не допуская побочных эффектов и внешних зависимостей. Таким образом, каждую написанную нами строку кода станет проще тестировать, даже если не удастся обойтись только лишь чистыми или идемпотентными функциями.
Знакомство с функциональным программированием в Python, JavaScript и Java
![]()
Функциональное программирование (ФП) представляет собой процесс создания ПО путем компоновки чистых функций. В современном мире работодатели ищут программистов, способных применять к решению задач различные парадигмы программирования. При этом наблюдается рост популярности именно функциональной, так как она очень эффективна и позволяет легко масштабировать проекты.
Как же можно половчее переключиться от ООП к ФП?
Сегодня мы изучим ключевые принципы функционального программирования, рассмотрим их реализацию в Python, JavaScript и Java, а также прикинем, в каком направлении лучше всего продолжать двигаться.
По ходу статьи мы ответим на следующие вопросы:
- Что такое функциональное программирование?
- Как оно реализовано в различных языках?
- Каковы его основные принципы?
- Как оно используется в Python, JavaScript и Java?
- Что стоит изучать далее?
Что такое функциональное программирование?
Функциональное программирование — это парадигма декларативного программирования, в которой программы создаются путем последовательного применения функций, а не инструкций.
Каждая из этих функций принимает входное значение и возвращает согласующееся с ним выходное значение, не изменяясь и не подвергаясь воздействию со стороны состояния программы.
Для таких функций предусмотрено выполнение только одной операции, если же требуется реализовать сложный процесс, то используется уже композиция функций, связанных последовательно. В процессе ФП мы создаем код, состоящий из множества модулей, поскольку функции в нем могут повторно использоваться в разных частях программы путем вызова, передачи в качестве параметров или возвращения.
Чистые функции не производят побочных эффектов и не зависят от глобальных переменных или состояний.
Функциональное программирование используется, когда решения легко выражаются с помощью функций и не имеют ощутимой связи с физическим миром. В то время как объектно-ориентированные программы моделируют код по образцу реальных объектов, ФП задействует математические функции, в которых промежуточные или конечные значения не сопоставляются с объектами физического мира.
К наиболее распространенным областям, применяющим ФП, относятся проектирование ИИ, алгоритмы классификации в МО, финансовые программы, а также продвинутые модели математических функций.
Проще говоря: функциональные программы выполняют много чистых однозадачных функций, совмещенных в последовательность для решения сложных математических или не связанных с физическим миром задач.
Преимущества функционального программирования
- Легкая отладка: чистые функции и неизменяемые данные упрощают обнаружение мест определения значений переменных. В чистых функциях меньше факторов, влияющих на них, что позволяет быстрее находить проблемные участки кода.
- Отложенное вычисление: функциональные программы производят вычисления только при необходимости. Это позволяет им повторно использовать ранее полученные результаты и экономить время на выполнение.
- Модульность: чистые функции не полагаются на внешние переменные или состояния, в связи с чем их можно легко переиспользовать в разных местах программы. Кроме того, функции будут выполнять только одну операцию или вычисление, что не позволит вам при их использовании случайно импортировать лишний код.
- Лучшая читаемость: функциональные программы легко читать, потому что поведение каждой функции неизменяемо и изолировано от состояния программы. В результате вы зачастую можете легко понять, что будет делать функция, просто по ее имени.
- Параллельное программирование: программы легче создавать при помощи функционального подхода, потому что неизменяемые переменные снижают число изменений внутри этих программ. Каждой функции приходится работать только с вводом пользователя, и она может быть уверена, что состояние программы в основном останется прежним.
Языки функционального программирования
Функциональная парадигма поддерживается не во всех языках. Некоторые из них, например Haskell, спроектированы именно для этой задачи, в то время как другие, например JavaScript, реализуют возможности и ООП, и ФП. Есть же и такие языки, где функциональное программирование невозможно в принципе.
Функциональные языки:
-
: это наиболее популярный язык среди функциональных программистов. В нем реализована защита памяти, отличный сбор мусора, а также повышенная скорость, обусловленная ранней компиляцией машинного кода. Его богатая статическая система типов дает вам доступ к уникальным алгебраическим и полиморфным типам, которые делают процесс программирования более эффективным, а код более читаемым. : этот язык, как и его потомок, Elixir, заняли нишу лучших функциональных языков для параллельных систем. Несмотря на то, что в популярности он уступает Haskell, его нередко используют для бэкенд-программирования. В последнее время Erlang начал завоевывать внимание в сфере масштабируемых мессенджеров, таких как WhatsApp и Discord. : это ориентированный на функциональную парадигму диалект Lisp, который работает на виртуальной машине Java (JVM). Будучи преимущественно функциональным языком, он поддерживает как изменяемые, так и неизменяемые структуры данных, но при этом все же менее строг в функциональном плане, чем другие. Если вам нравится Lisp, то вы также полюбите и Clojure. : этот язык аналогичен Haskell (они находятся в одной языковой группе), но имеет меньше расширенных возможностей. Кроме того, в нем реализована слабая поддержка объектно-ориентированных конструкций.
Языки с функциональными возможностями
-
: этот язык поддерживает как ООП, так и ФП. Его наиболее интересная особенность в наличии строгой системы статической типизации, как в Haskell, которая помогает создавать строгие функциональные программы. При проектировании Scala среди прочих стояла задача решить многие критические проблемы Java, поэтому данный язык очень подходит для Java-разработчиков, желающих попробовать функциональное программирование.
- JavaScript: несмотря на то, что приоритет в этом языке не на стороне функциональной парадигмы, JavaScript уделяет ей немало внимания в связи со своей асинхронной природой. В нем также поддерживаются такие важные функциональные возможности, как лямбда выражения и деструктуризация. Вместе эти атрибуты выделяют JS как ведущий язык для ФП.
- Python, PHP, C++: эти мультипарадигмальные языки тоже поддерживают функциональное программирование, но уже в меньшей степени, чем Scala и JavaScript.
- Java: этот язык относится к языкам общего назначения, но приоритет в нем отдается ООП, основанному на классах. Несмотря на то, что добавление лямбда выражений в некотором смысле помогает реализовывать более функциональный стиль, в конечном итоге Java остается языком ООП. Он позволяет заниматься функциональным программированием, но при этом в нем недостает ключевых элементов, которые бы оправдывали его освоение именно с этой целью.
Принципы функционального программирования
Переменные и функции
Ключевыми составляющими функциональной программы являются уже не объекты и методы, а переменные и функции. При этом следует избегать глобальных переменных, потому что изменяемые глобальные переменные усложняют понимание программы и ведут к появлению у функций побочных эффектов.
Чистые функции
Для чистых функций характерны два свойства:
- они не создают побочных эффектов;
- они всегда производят одинаковый вывод при получении одинакового ввода, что еще можно называть как ссылочную прозрачность.
Побочные эффекты же возникают, если функция изменяет состояние программы, переписывает вводную переменную или в общем вносит какие-либо изменения при генерации вывода. Отсутствие же побочных эффектов снижает риски появления ошибок по вине чистых функций.
Ссылочная прозрачность означает, что любой вывод функции должен допускать замену на ее значение, не изменяя при этом результата программы. Этот принцип гарантирует, что вы создаете такие функции, которые выполняют только одну операцию и достигают согласованного вывода.
Ссылочная прозрачность возможна только, если функция не влияет на состояние программы или в общем не старается выполнить более одной операции.
Неизменяемость и состояния
Неизменяемые данные или состояния не могут изменяться после их определения, что позволяет сохранять постоянство стабильной среды для вывода функций. Лучше всего программировать каждую функцию так, чтобы она выводила один и тот же результат независимо от состояния программы. Если же она зависит от состояния, то это состояние должно быть неизменяемым, чтобы вывод такой функции оставался постоянным.
Подходы функционального программирования обычно избегают применения функций с общим состоянием (когда несколько функций опираются на одно состояние) и функций с изменяющимся состоянием (которые зависят от изменяемых функций), потому что они уменьшают модульность программы. Если же вы не можете обойтись без функций с общим состоянием, сделайте это состояние неизменяемым.
Рекурсия
Одно из серьезных отличий объектно-ориентированного программирования от функционального в том, что программы последнего избегают таких конструкций, как инструкции if else или циклы, которые в разных случаях выполнения могут выдавать разные выводы.
Вместо циклов функциональные программы используют для всех задач по перебору рекурсию.
Функции первого класса
Функции в ФП рассматриваются как типы данных и могут использоваться как любое другое значение. Например, мы заполняем функциями массивы, передаем их в качестве параметров или сохраняем их в переменных.
Функции высшего порядка
Эти функции могут принимать другие функции в качестве параметров или возвращать функции в качестве вывода. Они делают возможности вызова функций более гибкими и позволяют легче абстрагироваться от действий.
Композиция функций
Для выполнения сложных операций функции можно выполнять последовательно. В этом случае результат каждой функции передается следующей функции в виде аргумента. Это позволяет с помощью всего одного вызова функции активировать целую серию их последовательных вызовов.
Функциональное программирование в Python
В Python реализована частичная поддержка ФП, и некоторые используемые в нем решения математических программ легче реализуются с помощью именно функционального подхода.
Самая сложная часть перехода к использованию такого подхода в сокращении числа используемых классов. В Python классы имеют изменяемые атрибуты, что усложняет создание чистых неизменяемых функций.
Попробуйте оформлять весь код на уровне модулей и переключайтесь на классы только по мере необходимости.
Давайте посмотрим, как добиться чистых неизменяемых функций и функций первого класса в Python, после чего познакомимся с синтаксисом для их композиции.
Чистые и неизменяемые функции
Многие из встроенных в Python структур данных являются неизменяемыми по умолчанию:
- integer;
- float;
- Boolean;
- string;
- Unicode;
- tuple.
Кортежи особенно полезны при использовании в качестве неизменяемой формы массива.
Этот код вызывает ошибку, потому что старается переопределить неизменяемый объект кортежа. Эти неизменяемые структуры данных рекомендуется использовать в функциональных программах Python для получения чистых функций.
Нижеприведенную функцию можно считать чистой, так как у нее нет побочных эффектов, и она всегда возвращает одинаковый вывод:
Функции первого класса
Отметим, что в Python функции рассматриваются как объекты, и ниже мы приводим краткое руководство по их возможному использованию:
Функции в качестве объектов
Передача функции в качестве параметра
Возвращение функции из другой функции
Композиция функций
Для компоновки функций в Python мы используем вызов lambda function . Это позволяет нам единовременно вызывать любое число аргументов.
На строке 4 мы определяем функцию compose2 , получающую две функции в качестве аргументов f и g .
На строке 5 мы возвращаем новую функцию, представляющую композицию из f и g .
В завершении на строке 6 мы возвращаем результаты этой композиции функций.
Функциональное программирование в JavaScript
В связи с поддержкой функций первого класса JavaScript уже давно предлагает функциональные возможности. ФП на этом языке с недавних пор начало набирать популярность, так как повышает производительность при использовании в таких фреймворках, как Angular и React.
Давайте взглянем на то, как можно реализовывать разные функциональные принципы с помощью JS. Сосредоточимся мы на создании ключевых компонентов, а именно чистых функций, функций первого класса и композиций функций.
Чистые и неизменяемые функции
Чтобы начать создание чистых функций в JS, нам понадобится использовать функциональные альтернативы стандартному поведению, такие как const , concat и filter .
Стандартное ключевое слово let определяет изменяемую переменную. Если вместо него для объявления использовать const , это гарантирует нам неизменность переменной, так как переназначить ее уже не получится.
Функциональные альтернативы нам также нужно использовать для управления массивами. Стандартным способом добавления элемента в массив является метод push() . К сожалению, этот метод изменяет начальный массив, в связи с чем не считается чистым.
Но у нас есть его функциональный эквивалент — concat() . Вот он уже возвращает новый массив, который содержит все начальные элементы вместе с добавленным. В этом случае сам начальный массив остается неизменным.
Для удаления элемента из массива мы обычно используем методы pop() и slice() . Тем не менее они не относятся к функциональным, так как изменяют именно первичный массив. Вместо них мы берем метод filter() , который создает новый массив со всеми элементами, прошедшими проверку условия.
Функции первого класса
JavaScript поддерживает функции первого класса по умолчанию. Вот краткое руководство по возможным действиям с функциями в этом языке:
Присвоение функции к переменной
Добавление функции в массив
Передача функции в качестве аргумента
Возвращение функции из другой функции
Функциональная композиция
В JavaScript мы можем компоновать функции при помощи цепочек вызовов:
В качестве альтернативы можно передать выполнение функции в следующую функцию:
Если же требуется связать больше функций, можно вместо этого использовать библиотеку lodash , которая позволит упростить их композицию. Если точнее, то мы передаем в качестве аргумента ее метод compose , сопровождаемый списком функций.
Первая функция в этом списке использует в качестве ввода начальный аргумент, а последующие функции наследуют свои вводные аргументы из вывода предшествующих.
Функциональное программирование в Java
Java очень ограниченно поддерживает ФП по сравнению с Python или JS. Тем не менее в нем есть возможность имитировать функциональное поведение при помощи лямбда функций, потоков и анонимных классов.
В конце концов, компилятор Java создавался без учета функционального программирования, в связи с чем не может использовать многие из преимуществ этой парадигмы.
Чистые и неизменяемые функции
В Java есть несколько неизменяемых структур данных:
- integer;
- Boolean;
- byte;
- short;
- string.
Вы также можете создавать собственные неизменяемые классы при помощи ключевого слова final .
Ключевое слово final в классе предотвращает создание дочернего класса. Использование final для name и regNo делает невозможным изменение значений после построения объекта.
В этом классе также присутствуют параметризованный конструктор и геттеры для всех переменных, но при этом отсутствуют сеттеры, что помогает добиться для данного класса неизменяемости.
Функции первого класса
В Java для получения функций первого класса можно использовать лямбда функции. Лямбда принимает список выражений, например методов, но не требует имени или предварительного определения.
Лямбда выражения можно использовать вместо функций, так как они рассматриваются как стандартные объекты класса, которые можно передавать или возвращать.
Композиция функций
Java содержит интерфейс, java.util.function.Function , предоставляющий методы для композиции функций. Метод compose сначала выполняет переданную ему функцию ( multiplyByTen ), а затем передает возвращаемое значение внешней функции ( square ).
И наоборот — метод andThen выполняет сначала внешнюю функцию, а затем функцию из своих параметров.
На строках 1 и 2 мы сначала создаем две функции, square и multiplyByTen .
Затем на строках 5 и 8 мы делаем из них две композиции, multiplyByTenAndSquare и squareAndMultiplyByTen , каждая из которых принимает два аргумента (удовлетворяя условие square ).
Каждая из этих композиций выполняет обе изначальные функции, но в разном порядке. Теперь вы можете вызвать композиции для выполнения обеих исходных функций с одинаковым вводом.
Что изучать дальше
Сегодня мы пробежались по наиболее общим принципам функционального программирования и узнали, как они проявляются в Python, JavaScript и Java.
Одним из ведущих функциональных языков, переживающим этап возрождения, является Scala. Многие технологические гиганты, такие как Twitter и Facebook, начали использовать этот язык и уже ищут программистов с соответствующими навыками, поэтому рекомендуем выбрать в качестве следующего этапа на пути освоения ФП именно Scala.
Rukovodstvo
статьи и идеи для разработчиков программного обеспечения и веб-разработчиков.
Функциональное программирование на Python
Введение Функциональное программирование — это популярная парадигма программирования, тесно связанная с математическими основами информатики. Хотя нет строгого определения того, что представляет собой функциональный язык, мы считаем их языками, которые используют функции для преобразования данных. Python не является функциональным языком программирования, но он включает в себя некоторые из своих концепций наряду с другими парадигмами программирования. С помощью Python легко писать код в функциональном стиле, который может обеспечить
Время чтения: 9 мин.
Вступление
Функциональное программирование — это популярная парадигма программирования, тесно связанная с математическими основами информатики. Хотя нет строгого определения того, что представляет собой функциональный язык, мы считаем их языками, которые используют функции для преобразования данных.
Python не является функциональным языком программирования, но он включает в себя некоторые из своих концепций наряду с другими парадигмами программирования. С Python легко написать код в функциональном стиле, который может обеспечить лучшее решение поставленной задачи.
Концепции функционального программирования
Функциональные языки — это декларативные языки, они сообщают компьютеру, какой результат они хотят. Это обычно контрастирует с императивными языками, которые говорят компьютеру, какие шаги нужно предпринять для решения проблемы. Python обычно кодируется императивно, но при необходимости может использовать декларативный стиль.
На некоторые функции Python повлиял чисто функциональный язык программирования Haskell. Чтобы лучше понять, что такое функциональный язык, давайте рассмотрим возможности Haskell, которые можно рассматривать как желательные функциональные черты:
- Чистые функции — не имеют побочных эффектов, то есть не меняют состояние программы. При одном и том же вводе чистая функция всегда будет производить один и тот же вывод.
- Неизменяемость — данные не могут быть изменены после создания. Возьмем, к примеру, создание List из трех элементов и сохранение его в переменной my_list . Если my_list неизменяемый, вы не сможете изменять отдельные элементы. Вам нужно будет установить my_list в новый List если вы хотите использовать другие значения.
- Функции высшего порядка — функции могут принимать другие функции в качестве параметров, а функции могут возвращать новые функции в качестве выходных данных. Это позволяет нам абстрагироваться от действий, давая нам гибкость в поведении нашего кода.
Haskell также повлиял на итераторы и генераторы в Python своей отложенной загрузкой, но эта функция не является необходимой для функционального языка.
Функциональное программирование на Python
Без каких-либо специальных функций или библиотек Python мы можем начать кодирование более функциональным способом.
Чистые функции
Если вы хотите, чтобы функции были чистыми, не изменяйте значение ввода или любые данные, которые существуют за пределами области действия функции.
Это значительно упрощает тестирование функции, которую мы пишем. Поскольку он не меняет состояние какой-либо переменной, мы гарантированно получаем один и тот же результат каждый раз, когда запускаем функцию с одним и тем же входом.
Давайте создадим чистую функцию для умножения чисел на 2:
Исходный список numbers не изменился, и мы не ссылаемся на какие-либо другие переменные вне функции, поэтому он чистый.
Неизменность
У вас когда-нибудь была ошибка, когда вы задавались вопросом, как переменная, установленная вами на 25, стала None ? Если бы эта переменная была неизменной, ошибка возникла бы там, где переменная была изменена, а не там, где измененное значение уже повлияло на программное обеспечение — основную причину ошибки можно найти ранее.
Python предлагает несколько неизменяемых типов данных, популярным из которых является Tuple . Давайте сравним кортеж со списком , который является изменяемым:
Вы увидите TypeError: ‘tuple’ object does not support item assignment .
Теперь есть интересный сценарий, когда Tuple может показаться изменяемым объектом. Например, если мы хотим изменить список в immutable_collection с [4, 5] на [4, 5, 6] , вы можете сделать следующее:
Это работает, потому что List является изменяемым объектом. Попробуем изменить список обратно на [4, 5] .
Он терпит неудачу, как мы и ожидали. Хотя мы можем изменить содержимое изменяемого объекта в Tuple , мы не можем изменить ссылку на изменяемый объект, который хранится в памяти.
Функции высшего порядка
Напомним, что функции высшего порядка либо принимают функцию в качестве аргумента, либо возвращают функцию для дальнейшей обработки. Давайте проиллюстрируем, как просто и то, и другое можно создать в Python.
Рассмотрим функцию, которая печатает строку несколько раз:
Что, если бы мы хотели 5 раз записать в файл или 5 раз записать сообщение? Вместо того, чтобы писать 3 разные функции, которые все циклически повторяются, мы можем написать 1 функцию высшего порядка, которая принимает эти функции в качестве аргумента:
Теперь представьте, что нам поручено создать функции, увеличивающие числа в списке на 2, 5 и 10. Начнем с первого случая:
Хотя писать функции add5 и add10 , очевидно, что они будут работать одинаково: перебирать список и добавлять инкрементатор. Поэтому вместо того, чтобы создавать множество различных функций приращения, мы создаем 1 функцию высшего порядка:
Функции высшего порядка придают нашему коду гибкость. Абстрагируя, какие функции применяются или возвращаются, мы получаем больший контроль над поведением нашей программы.
Python предоставляет несколько полезных встроенных функций высшего порядка, которые значительно упрощают работу с последовательностями. Сначала мы рассмотрим лямбда-выражения, чтобы лучше использовать эти встроенные функции.
Лямбда-выражения
Лямбда-выражение — это анонимная функция. Когда мы создаем функции в Python, мы используем def и даем ему имя. Лямбда-выражения позволяют нам определять функцию намного быстрее.
Давайте создадим функцию высшего порядка hof_product которая возвращает функцию, которая умножает число на предопределенное значение:
Лямбда-выражение начинается с ключевого слова lambda за которым следуют аргументы функции. После двоеточия следует код, возвращаемый лямбдой. Эта возможность создавать функции «на ходу» активно используется при работе с функциями высшего порядка.
Если вам нужна дополнительная информация, мы рассмотрим гораздо больше лямбда-выражений, которые мы рассмотрим в нашей статье « Лямбда-функции в Python».
Встроенные функции высшего порядка
Python реализовал некоторые часто используемые функции высшего порядка из языков функционального программирования, что значительно упрощает обработку повторяемых объектов, таких как списки и итераторы. По причинам эффективности использования пространства / памяти эти функции возвращают iterator вместо списка.
карта
Функция map позволяет нам применять функцию к каждому элементу в повторяемом объекте. Например, если у нас есть список имен и мы хотим добавить приветствие к строкам, мы можем сделать следующее:
Фильтр
Функция filter проверяет каждый элемент в итеративном объекте с помощью функции, которая возвращает либо True либо False , сохраняя только те True . Если бы у нас был список чисел и мы хотели бы сохранить те, которые делятся на 5, мы можем сделать следующее:
Объединение map и filter
Поскольку каждая функция возвращает итератор, и обе они принимают итерируемые объекты, мы можем использовать их вместе для некоторых действительно выразительных манипуляций с данными!
Выражение в arbitrary_numbers можно разбить на 3 части:
- range(1, 21) — это повторяемый объект, представляющий числа от 1, 2, 3, 4 . 19, 20.
- filter(lambda num: num % 3 == 0, range(1, 21)) — итератор для числовой последовательности 3, 6, 9, 12, 15 и 18.
- Когда они построены в кубе map мы можем получить итератор для числовой последовательности 27, 216, 729, 1728, 3375 и 5832.
Составить список
Популярная функция Python, которая занимает видное место в языках функционального программирования, — это составление списков. Подобно map и filter , списки позволяют изменять данные в сжатой и выразительной форме.
Давайте вместо этого попробуем наши предыдущие примеры с map и filter со списком:
Базовое понимание списка следует этому формату: [результат for единственного-элемента in списка].
Если мы хотим отфильтровать объекты, нам нужно использовать ключевое слово if
Каждое map и filter может быть выражено в виде списка.
Некоторые моменты, которые следует учитывать
Хорошо известно, что создатель Python, Гвидо ван Россум, не планировал, чтобы Python имел функциональные возможности, но оценил некоторые преимущества, которые его введение принесло языку. Он обсудил историю возможностей языка функционального программирования в одном из своих сообщений в блоге . В результате реализации языка не были оптимизированы для функций функционального программирования.
Более того, сообщество разработчиков Python не поощряет использование огромного количества функций функционального программирования. Если бы вы писали код для рассмотрения глобальным сообществом Python, вы бы написали понимание списков вместо использования map или filter . Лямбды будут использоваться минимально, как вы назвали бы свои функции.
В интерпретаторе Python введите import this и вы увидите «Дзен Python». Python обычно поощряет писать код наиболее очевидным способом. В идеале весь код должен быть написан одним способом — сообщество не считает, что он должен быть в функциональном стиле.
Заключение
Функциональное программирование — это парадигма программирования, в которой программное обеспечение в основном состоит из функций, обрабатывающих данные на протяжении всего своего выполнения. Хотя нет единого определения того, что такое функциональное программирование, мы смогли изучить некоторые характерные особенности функциональных языков: чистые функции, неизменяемость и функции высшего порядка.
Python позволяет нам писать код в функциональном декларативном стиле. Он даже поддерживает многие общие функциональные возможности, такие как лямбда-выражения, а также функции map и filter .
Однако сообщество Python не всегда рассматривает использование методов функционального программирования передовой практикой. Тем не менее, мы узнали новые способы решения проблем, и при необходимости можем решать проблемы, используя выразительность функционального программирования.