Основные принципы функционального программирования
Основные принципы программирования: функциональное программирование
- Переводы, 23 января 2017 в 13:43
Если вы такой же разработчик, как и я, то наверняка сперва изучали парадигму ООП. Первым вашим яыком были Java или C++ — или, если вам повезло, Ruby, Python или C# — поэтому вы наверняка знаете, что такое классы, объекты, экземпляры и т.д. В чём вы точно не особо разбираетесь, так это в основах той странной парадигмы, называющейся функциональным программированием, которая существенно отличается не только от ООП, но и от процедурного, прототипно-ориентированного и других видов программирования.
Функциональное программирование становится популярным — и на то есть причины. Сама парадигма не нова: Haskell, пожалуй, является самым функциональным языком, а возник он в 90-ых. Такие языки, как Erlang, Scala, Clojure также попадают под определение функциональных. Одним из основных преимуществ функционального программирования является возможность написания программ, работающих конкурентно (если вы уже забыли, что это — освежите память прочтением статьи о конкурентности), причём без ошибок — то есть взаимные блокировки и потокобезопасность вас не побеспокоят.
У функционального программирования есть много преимуществ, но возможного максимального использования ресурсов процессора благодаря конкурентному поведению — это его главный плюс. Ниже мы рассмотрим основные принципы функционального программирования.
Вступление: Все эти принципы не обязательны (многие языки следуют им не полностью). Все они теоретические и нужны для наиболее точного определения функциональной парадигмы.
1. Все функции — чистые
Это правило безусловно является основным в функциональном программировании. Все функции являются чистыми, если они удовлетворяют двум условиям:
- Функция, вызываемая от одних и тех же аргументов, всегда возвращает одинаковое значение.
- Во время выполнения функции не возникают побочные эффекты.
Первое правило понятно — если я вызываю функцию sum(2, 3) , то ожидаю, что результат всегда будет равен 5. Как только вы вызываете функцию rand() , или обращаетесь к переменной, не определённой в функции, чистота функции нарушается, а это в функциональном программировании недопустимо.
Второе правило — никаких побочных эффектов — является более широким по своей природе. Побочный эффект — это изменение чего-то отличного от функции, которая исполняется в текущий момент. Изменение переменной вне функции, вывод в консоль, вызов исключения, чтение данных из файла — всё это примеры побочных эффектов, которые лишают функцию чистоты. Может показаться, что это серьёзное ограничение, но подумайте ещё раз. Если вы уверены, что вызов функции не изменит ничего “снаружи”, то вы можете использовать эту функцию в любом сценарии. Это открывает дорогу конкурентному программированию и многопоточным приложениям.
2. Все функции — первого класса и высшего порядка
Эта концепция — не особенность ФП (она используется в Javascript, PHP и других языках) — но его обязательное требование. На самом деле, на Википедии есть целая статья, посвящённая функциям первого класса. Для того, чтобы функция была первоклассной, у неё должна быть возможность быть объявленной в виде переменной. Это позволяет управлять функцией как обычным типом данных и в то же время исполнять её.
Функции высшего порядка же определяются как функции, принимающие другую функцию как аргумент или возвращающие функцию. Типичными примерами таких функций являются map и filter.
3. Переменные неизменяемы
Тут всё просто. В функциональном программировании вы не можете изменить переменную после её инициализации. Вы можете создавать новые, но не можете изменять существующие — и благодаря этому вы можете быть уверены, что никакая переменная не изменится.
4. Относительная прозрачность функций
Сложно дать корректное определение относительной прозрачности. Самым точным я считаю такое: если вы можете заменить вызов функции на возвращаемое значение, и состояние при этом не изменится, то функция относительно прозрачна. Это, быть может, очевидно, но я приведу пример.
Пусть у нас есть Java-функция, которая складывает 3 и 5:
Очевидно, что любой вызов этой функции можно заменить на 8 — значит, функция относительно прозрачна. Вот пример непрозрачной функции:
Эта функция ничего не возвращает, но печатает текст, и при замене вызова функции на ничто состояние консоли будет другим — значит, функция не является относительно прозрачной.
5. Функциональное программирование основано на лямбда-исчислении
Функциональное программирование сильно опирается на математическую систему, называющуюся лямбда-исчислением. Я не математик, поэтому я не буду углубляться в детали — но я хочу обратить внимание на два ключевых принципа лямбда-исчисления, которые формируют самое понятие функционального программирования:
- В лямбда-исчислении все функции могут быть анонимными, поскольку единственная значимая часть заголовка функции — это список аргументов.
- При вызове все функции проходят процесс каррирования. Он заключается в следующем: если вызывается функция с несколькими аргументами, то сперва она будет выполнена лишь с первым аргументом и вернёт новую функцию, содержащую на 1 аргумент меньше, которая будет немедленно вызвана. Этот процесс рекурсивен и продолжается до тех пор, пока не будут применены все аргументы, возвращая финальный результат. Поскольку функции являются чистыми, это работает.
Как я уже говорил, лямбда-исчисление на этом не заканчивается — но мы рассмотрели лишь ключевые аспекты, связанные с ФП. Теперь, в разговоре о функциональном программировании вы сможете блеснуть словечком “лямбда-исчисление”, и все подумают, что вы шарите 🙂
Заключение
Функциональное программирование серьёзно напрягает мозги — но это очень мощный подход, и я считаю, что его популярность будет только расти.
Если вы хотите узнать о функциональном программировании побольше, то советуем вам ознакомиться с примерами использования принципов ФП в JavaScript (часть 1, часть 2), а также с циклом статей, посвящённым функциональному C#.
Обязательно изучите функциональное программирование в 2017 году
Функциональное программирование существует уже очень давно, начиная с появления языка программирования Lisp в 50-х годах прошлого века. И, если вы заметили, на протяжении последних двух лет такие языки, как Clojure, Scala, Erlang, Haskell и Elixir, создают много шума и привлекают к себе внимание.
Но все-таки, что такое функциональное программирование? Почему все сходят с ума от него, но использующих его людей не становится больше? В этой статье я попытаюсь ответить на все эти вопросы и, надеюсь, заразить вас идеей функционального программирования.
Краткая история функционального программирования
Как мы уже говорили, функциональное программирование берет свое начало еще в 50-х годах с момента создания Lisp для работы в серии научных компьютеров IBM700/7000. Lisp представил множество парадигм и особенностей, которые теперь мы связываем с функциональным программированием, и хотя мы можем назвать Lisp дедушкой функционального программирования мы можем копнуть глубже и взглянуть на еще большую общность между всеми функциональными языками программирования — лямбда-исчисление.
Это, безусловно, самый интересный аспект функционального программирования. Все языки функционального программирования основаны на одной и той же простой математической основе — лямбда-исчислении.
Лямбда-исчисление обладает свойством полноты по Тьюрингу, то есть является универсальной моделью вычислений, которая может быть использована для моделирования любой одноленточной машины Тьюринга. Ее тезка, греческая буква лямбда (λ), используется в лямбда-выражениях и лямбда-условиях для обозначения связывания переменной с функцией. — Википедия
Лямбда-исчисление — удивительно простая, но мощная концепция. В основе лямбда-исчисления лежат два понятия:
- Функциональная абстракция, использующаяся для обобщения выражений посредством введения имен (переменных)
- Функциональное применение, которое используется для вычисления обобщенных выражений путем присвоения переданных имен к определенным значениям
В качестве примера давайте рассмотрим функцию f с одним аргументом, увеличивающую аргумент на единицу:
Допустим, мы хотим применить функцию к числу 5 . Тогда функцию можно читать следующим образом:
Основы функционального программирования
Пока что закончим с математикой. Давайте взглянем на особенности, делающие функциональное программирование таким мощным.
Функции первого класса
В функциональных языках функции являются объектами первого класса. Это означает, что функция может храниться в переменной. Например, в Elixir это так:
Затем мы легко можем вызвать эту функцию:
Функции высшего порядка
Функции высшего порядка — функции, принимающие одну или несколько функций в качестве аргументов и/или возвращающие новую функцию. Для демонстрации концепции давайте снова воспользуемся нашей функцией double :
В этом примере Enum.map в качестве первого аргумента принимает перечисляемый — список, а в качестве второго — функцию, которую мы только что определили. Затем Enum.map применяет функцию к каждому элементу списка. В результате мы получаем:
Неизменяемое состояние
В языках функционального программирования состояние неизменяемое: после того, как переменная привязана к значению, она не может быть переопределена. Это отлично для предотвращения побочных эффектов и состояния гонки, что делает работу с конкурентностью намного проще.
Как и прежде, давайте воспользуемся Elixir для иллюстрации:
В примере выше наша переменная tuple никогда не изменит своего значения. В третьей строке put_elem возвращает совершенно новый tuple без изменения значения оригинала.
Я не буду продолжать вдаваться в подробности, потому что эта статья не является введением в лямбда-исчисление, теорию вычислений или даже функциональное программирование. Если вы хотите, чтобы я копал глубже по любой из этих тем, напишите об этом в разделе комментариев. На данный момент мы можем закрепить следующее:
- Функциональное программирование существует уже давно (с начала 50-х годов)
- Функциональное программирование основано на математических концепциях, в частности на лямбда-исчислениях
- Функциональное программирование считалось слишком медленным по сравнению с императивными языками
- Функциональное программирование возвращается
Применение функционального программирования
Как разработчики программного обеспечения мы живем в захватывающие времена, когда обещанные облачные вычисления наконец-то здесь и каждому из нас доступен беспрецедентный объем компьютерной мощности. К сожалению, с этим также пришли и требования масштабируемости, производительности и параллелизма.
Объектно-ориентированное программирование уже не справляется, особенно когда речь идет о конкурентности и параллелизме. Попытки добавить их к этим языкам, добавляют много сложностей и чаще всего приводят к чрезмерному усложнению и низкой производительности.
С другой стороны, функциональное программирование уже хорошо подходит для таких задач как: неизменяемое состояние, замыкания и функции высокого порядка — концепции, очень хорошо подходящие для написания высоконагруженных и распределенных приложений.
Но не надо верить мне на слово, вы можете найти достаточно доказательств, посмотрев на технологические новости стартапов, таких как WhatsApp и Discord:
- 900 миллионов пользователей WhatsApp поддерживают всего лишь 50 инженеров, используя Erlang
- Discord подобным образом обрабатывают более миллиона запросов в минуту с использованием Elixir
Эти компании и команды справляются с взрывным ростом благодаря преимуществам функционального программирования. А поскольку функциональное программирование приобретает все большую популярность, я твердо верю, что подобные истории будут встречаться чаще.
По этой причине функциональное программирование обязательно должно быть в арсенале знаний каждого разработчика. Вам необходимо быть готовым к созданию приложений следующего поколения, которые будут обслуживать следующий миллиард пользователей. И, черт возьми, если этого было недостаточно, поверьте мне, функциональное программирование — это действительно весело, просто взгляните на Elixir:
OpenSource в заметках
Масса разработчиков программного обеспечения любят поговорить о функциональном программировании, однако если вы спросите их о том, применяли ли они его где-нибудь, в большинстве случаев в ответ вы получите «нет». Причина такой ситуации довольно проста. Когда мы учились программировать, разглядывая блок-схемы и размышляя о том, что и на каком шаге должна делать программа, мы выработали привычку мыслить императивно. В этой статье автор поделится с вами основными принципами, лежащими в основе функционального программирования, а также способами его применения в PHP.
Основные понятия функционального программирования
Начнём с определения. Википедия сообщает, что
«Функциональное программирование — раздел дискретной математики и парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).»
В функциональном программировании функции являются объектами первого класса (first-class citizen), в то время как в императивном программировании мы имеем дело в основном с переменными и пошаговым изменением их значений с целью получить требуемое состояние системы.
Когда говорят, что функция является «объектом первого класса», это означает что мы можем использовать функцию точно таим же образом, как мы используем переменную в императивном программировании. То есть, функции можно передавать в качестве параметров при вызове других функций, объявлять внутри других функций и даже возвращать функции в качестве значения других функций. Если попытаться выразиться короче, то «функция — это значение».
О’кей, вернёмся к этому позже, а пока что рассмотрим несколько ключевых понятий функционального программирования.
Неизменность
Неизменность — это поведение, при котором значение переменной не моет быть изменено после того, как оно определено. В разных языках это реализуется по-разному, а в PHP в частности для этого используются константы.
Рекурсия
Рекурсия — частое явление в функциональном программировании. В императивном программировании мы можем пользоваться циклами всякий раз, когда мы имеем дело с коллекциями или массивами, перебирая элементы и пользуясь временной переменной, чтобы сохранять промежуточные значения. Но при функциональном подходе такое сделать не получится по причине наличия принципа неизменности. Здесь на помощь придут рекурсия и стек вызова функций.
Допустим, нам потребовалось создать функцию, вычисляющую сумму всех элементов массива (все дружно делаем вид, что array_sum () не существует). Следуя духу функционального программирования, наша реализация выглядела бы примерно так:
Чистые функции и ссылочная прозрачность
Если функция не изменяет внешних по отношению к ней объектов, в том числе и не выполняет никаких операций ввода-вывода (в файл, базу данных, etc), то такую функцию называют «функцией без побочных эффектов» или «чистой функцией». Так, например, все математические функции являются чистыми, в то время как функции вроде date () и rand () — нет.
Значение, возвращаемое чистой функцией, будет всегда одинаково для одного и того же набора аргументов. Это приводит к следующему свойству, называемому «ссылочная прозрачность». Если функция «ссылочно-прозрачна», то мы можем заменить её, на возвращаемое ею значение, и это никак не отразиться на работе программы.
Функции высшего порядка
Концепции, описанные выше, могут быть реализованы практически в любом языке программирования, но чистые функции и функции высшего порядка — это два момента, отличительные для функциональных языков. Выше автор объяснил, что функции первого класса могут интерпретироваться в программе наравне со значениями. Функции же высшего порядка, это такие функции, которые могут принимать другие функции в качестве параметров вызова и возвращать функции в качестве значений. Относительно недавно в PHP были добавлены новые возможности, позволяющие создавать функции высшего порядка: лямбда-функции и замыкания.
Лямбда-функции
Лямбда-функция (также известная как анонимная функция) — это функция, у которой нет имени. Когда объявляется лямбда-функция, вы получаете ссылку на неё, которую можно сохранить в переменной с целью дальнейшего использования. То есть, вы можете использовать эту переменную везде, где вам потребуется вызов функции.
Такая возможность существует во многих языках. Одним из знакомых примеров для вас, скорее всего, окажется JavaScript, в котором лямбда-функции обычно используются в качестве callback-обработчиков событий.
PHP обзавёлся лямбда-функциями с выходом версии 5.3, и это предоставило возможность разработчиком создавать конструкции вида:
Говоря о функциях, а особенно о лямбда-функциях, важно понимать, как функционирует область видимости переменных. Например, JavaScript, позволяет вам обращаться во внешнюю по отношению к лямбда-функции область видимости, в то время как PHP этого не даст сделать. Внутри лямбда-функции в PHP вы можете получить доступ только области видимости функции. То есть, в этом аспекте своей работы лямбда-функции ничем не отличаются от своих «традиционных» собратьев.
Замыкания
Иногда возникает необходимость обратиться к переменной, находящейся в родительской, по отношению к функции, области видимости. Замыкания — это почти то же самое, что и лямбда-функции с той лишь разницей, что изнутри замыканий вы можете обращаться к переменным, находящимся во внешней области видимости. Для того, чтобы сделать это, вам понадобиться ключевой слово use, появившееся также в версии PHP 5.3.
Частичные функции и карринг
В двух словах, частичная функция — это функция, создана на основе существующей функции и части её аргументов. В PHP частичные функции мы можем создавать при помощи замыканий. В примере ниже представлено вычисление объёма параллелепипеда на основе длины, высоты и ширины, передаваемых через аргументы функции. Все аргументы являются необязательными. Решение достигается за счёт того, что в случае недостатка одного или более аргументов, будет создана функция, принимающая недостающие значения.
Все аргументы являются необязательными. Сначала выполняется проверка, переданы ли все три аргумента. Если это так, что функция просто вычисляет и возвращает объём. в противном случае создаётся новая функция, которая устанавливает значения недостающих аргументов по своему усмотрению исходя из полученных ранее данных.
Например, нам нужно будет вычислять объёмы параллелепипедов, длина которых всегда равна десяти. Традиционным решением было бы передавать 10 первым аргументом при каждом вызове функции. Или же, пользуясь нашим решением, мы можем сначала создать частичную функцию, передав ей только одно значение, а в последующих вызовах передавать лишь два недостающих значения.
Карринг — это частный случай частичных функций, при котором функция, принимающая несколько аргументов, преобразуется в несколько функций, каждая из которых принимает один аргумент. То есть, вместо f (x,y,z) будет f (x)(y)(z) (хотя синтаксис PHP этого и не позволяет). Если вам интересны подробности реализации данного подхода, обратитесь к статье Timothy Boronczyk о карринге в PHP.
Преимущества и недостатки
Существует множество способов применения функционального программирования в PHP. Например, лямбда-функции широко применяются в качестве callback-функций. Так, например, в Slim Framework вы можете определить действие для роута следующим образом:
Безопасное программирование реализуется за счёт избегания состояний и изменяемых данных. Программируя функционально, вы должны создавать функции такими, чтобы каждая из них решала лишь одну задачу и не производила побочных эффектов. Такая парадигма, ставящая акцент на модульности и краткости функций, поможет сделать вашу программу проще для понимания.
Функциональное программирование также может помочь вам создавать код, который занят решением задач, не привнося накладных расходов на управление процессом вычислений (возьмите, хотя бы, рекурсию и сравните с необходимостью управлять счётчиками в циклах).
Однако не забывайте, что некоторые полезные штуки из функционального программирования вы не сможете реализовать в PHP по той простой причине. что этот язык изначально для этого не проектировался. Например, чистые функции являются отличными кандидатами для работы параллельных вычислениях, но сам PHP, к сожалению, для этого совершенно не годится.
Также имейте ввиду, что не всегда просто удаётся обходится одной рекурсией и ленивыми функциями, более того, они обычно требуют дополнительных ресурсов, что приводит к значительному снижению производительности в отдельных случаях. Иногда всё-таки гораздо эффективней разрабатывать программы в терминах изменяемости.
Возможно, наибольшим недостатком функционального программирования является довольно большой объём информации, который необходимо изучить мозгу, привыкшему к императивной парадигме программирования. Но несмотря ни на что, функциональное программирование крайне интересно, и изучая его, вы получите возможность увидеть решение некоторых задач в новом свете, что в свою очередь поможет вам продолжать расти как программисту. Функциональное программирование не есть панацея, но если грамотно и к месту им пользоваться, вы на порядок сможете улучшить читаемость и выразительность вашего кода.
Итоги
Функциональное программирование — это нечто большее, чем просто парадигма. Это определённый образ мышления при разработке программного обеспечения. Вы обязательно сможете найти моменты в ваших разработках, где функциональные приёмы, рассмотренные в этой статье, придутся к месту. Пробуйте, учитесь и получайте удовольствие!
5 функциональных языков программирования, которые вы должны знать
Если вы проводите какое-то время, читая о тенденциях программирования в интернете, вы наверняка услышите о функциональном программировании. Термин встречается довольно часто, но что он означает?
Даже если вы знаете, что такое функциональное программирование, вам может быть неясно, какие языки лучше всего подходят для него. Ведь не все языки программирования одинаковы. Хотя вы можете применять парадигмы функционального программирования на многих языках, есть еще некоторые, используя которые, вы будете чувствовать себя гораздо более комфортно.
Что Такое Функциональное Программирование?
Если вы хорошо знаете и любите, у вас есть преимущество в функциональном программировании. Это связано с тем, что парадигма функционального программирования рассматривает вычисления как математические функции.
В основном, функциональное программирование рассматривает функции и данные как неизменяемые. Вы передаете данные в функцию, и она обычно возвращает эти данные, преобразованные в какой-то другой тип данных. В функциональном программировании функция никогда не должна изменять исходные данные или состояние программы.
Есть сходство с философией Unix, что каждая программа должна делать одну вещь хорошо. Функция не должна касаться различных частей программы. Вместо этого она должна принимать данные на входа и давать значение на выходе.
В идеале, функции должны быть чистыми, когда это возможно в функциональном программировании. Это означает, что при одних и тех же входных данных выходные данные функции всегда будут оставаться одними и теми же.
Функциональное и объектно-ориентированное программирование
Это драматический отход от чего-то вроде объектно-ориентированного программирования. В объектно-ориентированном программировании часто имеется базовый объект с различными методами, предназначенными для изменения данных или состояния, которые являются частью этого объекта. Метод может даже изменять данные или состояние, если не указан явно.
В практических программах иногда это имеет смысл. Тем не менее, это может усложнить поддержку программы, так как не всегда ясно, что изменяет состояние или данные. Функциональное программирование первоначально использовалось в академических целях, но также может помочь решить множество задач.
1. Javascript
Некоторые языки программирования позволяют выполнять все функции функционального программирования, в то время как другие либо не сильно годятся для этого. JavaScript относится к первой категории. В то же время вы можете так же легко использовать объектно-ориентированный подход.
Тем не менее, есть много функциональных парадигм программирования, встроенных в JavaScript. Возьмем, например, функции более высокого порядка. Это функции, которые могут принимать другие функции в качестве аргументов.
JavaScript имеет несколько функций, которые работают с массивами, такими как map() , reduce(), filter() , и другими, все из которых являются функциями более высокого порядка.
В то время как ранние версии JavaScript имел некоторые проблемы с изменяемостью, более новые версии стандарта ECMAScript предоставляют исправления этой проблемы. Вместо ключевого слова catch-all var для определения переменных теперь есть const и let . Первая позволяет определить константы, как следует из названия. Вторая, let , ограничивает область переменной функцией, в которой она объявлена.
2. Python
Как и JavaScript, Python является обобщенным языком, с помощью которого можно использовать любое количество парадигм программирования. Python может иметь свои недостатки, но функциональное программирование не является одним из них. Существует даже введение в функциональное программирование в официальной документации Python.
Для начала, вы найдете много из тех же map() , filter() , reduce() , и подобных функций, упомянутых выше. Как и в JavaScript, это функции более высокого порядка, поскольку они принимают другие функции в качестве аргументов. В Python функциональное программирование имеет преимущество в виде lambda ключевого слова.
Лямбда-выражения можно использовать несколькими способами. Один из способов использовать его в качестве стенографии для простых функций. При назначении переменной можно вызывать лямбда-выражения точно так же, как и стандартную функцию Python. Реальное преимущество лямбда-выражений заключается в использовании их в качестве анонимных функций.
Анонимные функции работают на JavaScript и других языках из этого списка. Они особенно удобны при использовании с функциями более высокого порядка, так как вы можете определить их сразу. Без анонимных функций вам пришлось бы заранее определять даже простые добавления как специальные функции.
3. Clojure
В отличие от JavaScript и Python, Clojure может быть не совсем знакомым языком, даже среди программистов. В случае, если вы не знакомы с Clojure – этот язык является диалектом языка программирования Lisp, который придумали к концу 1950-х годов.
Как и другие диалекты Lisp, Clojure рассматривает код как данные. Это означает, что код может эффективно изменять себя. В отличие от других диалектов Lisp, Clojure работает на платформе Java и компилируется в байт-код JVM. Это означает, что он может работать с библиотеками Java, были ли они написаны на Clojure или нет.
В отличие от предыдущих языков в этом списке, Clojure изначально является функциональным языком программирования. Это означает, что он защищает неизменность везде, где это возможно, особенно в рамках структур данных.
4. Elm
Один из новых языков в этом списке, Elm-чисто функциональный язык, первоначально разработанный Evan Czaplicki в 2012 году. Язык приобрел популярность среди веб-разработчиков, в частности, для создания пользовательских интерфейсов.
В отличие от всех предыдущих в этом списке, Elm использует статическую проверку типов. Это помогает гарантировать отсутствие исключений во время выполнения, когда ошибки перехватываются во время компиляции. Это означает? что будет меньше видимых ошибок для пользователей, что является большим плюсом.
Компилятор Elm предназначен для HTML, CSS и JavaScript. Так же, как вы можете использовать Clojure для написания программ, которые работают на Java, вы можете писать приложения, которые используют библиотеки JavaScript в Elm.
Одно из главных отличий Elm от других языков заключается в том, что вы не найдете универсальных filter(), map() , и похожих функций. Вместо этого они определяются типом данных, таким как List.map или Dict.map .
5. Haskell
Haskell – это другой статически типизированный, чисто функциональный язык. В отличие от Elm, Haskell существует уже довольно долго. Первая версия языка была разработана в 1990 году. Последний стандарт-Haskell 2010, а следующая версия запланирована на 2020 год.
Как мы уже знаем, чисто функциональная природа Haskell означает, что по замыслу, функции не должны иметь побочных эффектов. Это делает его хорошо подходящим для решения реальных проблем, несмотря на корни функционального программирования в академических кругах.
Несмотря на отсутствие популярности, Haskell был использован в некоторых широко используемых проектах. Оконный менеджер Xmonad полностью написан на языке Haskell. Pandoc, который преобразует различные типы разметки в другие форматы..
Присутствуют стандартные map() , filter() , reduce() , и другие функции более высокого порядка, которые должны позволить вам взять концепции из JavaScript или Python в Haskell.
Вы новичок в программировании?
Некоторые из вышеперечисленных терминов и языков могут показаться несколько эзотерическими, если вы еще не опытный кодер. Это хорошо, так как знание того, чего вы не знаете, является одним из первых шагов в становлении хорошего специалиста.
Некоторые из перечисленных выше языков лучше подходят для начинающих, чем другие. Посмотрите на наш список лучших языков программирования для начинающих .
ЗАЧЕМ НУЖНО ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ
В результате изучения материала главы 1 студент должен:
- • особенности функционального стиля программирования;
- • недостатки и преимущества функционального стиля по отношению к императивному стилю программирования;
- • преобразовывать программы, написанные в императивном стиле, в программы, написанные в функциональном стиле;
- • оптимизировать функциональные программы для параллельных вычислений;
• навыками анализа кода на предмет соответствия его функциональному стилю программирования.
Особенности функционального стиля
Рассмотрим основные особенности функционального программирования. Часто процесс исполнения программы можно представить в виде схемы, показанной на рис. 1.1.
Рис. 1.1. Схема простой функциональной программы
Конечно, не любую программу можно представить в виде такой схемы, а только такую, которая, получив некоторые входные данные в начале своей работы, затем обрабатывает их, не общаясь с внешней средой, и в конце работы выдает некоторый результат. Часто такую схему работы программы называют «черным ящиком», подразумевая, что в ней нас интересует не то, как и в какой последовательности происходят те или иные действия в программе, а только то, какой результат получается в зависимости от входных данных. Можно сказать, что при такой схеме работы результат находится в функциональной зависимости от исходных данных.
Можно привести много примеров простых и сложных программ, имеющих смысл и работающих но этой схеме. Например, в программе аналитических преобразований выражений входными и выходными данными будут выражения, представленные в разной форме. Другой пример — компилятор с некоторого языка программирования: входными данными программы будет текст, написанный на этом языке программирования, а выходными данными — объектный код программы. Третий пример — программа составления расписания учебных занятий: исходные данные для такой программы — набор «пожеланий» к расписанию, а выходные данные — таблица, представляющая расписание занятий.
В то же время многие программы выполняются не по схеме «черного ящика». Например, любая программа, организующая активное взаимодействие (диалог) с пользователем, не является преобразователем набора входных данных в выходные, если только не считать, что входными и выходными данными в этом случае является «среда», включающая в том числе и пользователя.
Всякая программа, написанная в функциональном стиле, — эго программа, представляющая собой функцию, аргументом которой служат входные данные из некоторого допустимого набора и выдающую определенный результат. Обычно при этом подразумевается, что функция, определяющая поведение программы, на одних и тех же входных данных всегда выдает один и тот же результат, т.е. это функция детерминированная. Можно заметить, что любая программа, содержащая запросы к пользователю (взаимодействующая с пользователем) — не детерминированная, поскольку ее поведение может зависеть от «поведения пользователя», т.е. конкретных данных, введенных пользователем.
В данной главе мы изучим основы языка функционального программирования Haskell, который используется для написания программ в виде функций. Отличительными особенностями функций, определяемых любым языком функционального программирования, являются следующие:
- • каждая функция в программе выдает один и тот же результат на одном и том же наборе входных данных (аргументов функции), т.е. результат работы функции является «повторяемым»;
- • вычисление функции не может повлиять на результат работы других функций, т.е. функции являются «чистыми».
Замечание. Как требование детерминированности, так и требование «чистоты» на практике приводят к тому, что некоторые части реальных программ по сути не являются функциональными. Эти части можно обособить достаточно аккуратно, чтобы функциональный и «нефункциональный» стили программирования не смешивались. Например, в языке Haskell для этого используются специальные монады, выводящие за рамки чисто функционального мира.
Если программа состоит только из чистых функций, то порядок вычисления аргументов этих функций будет несущественным. Вообще, любые выражения, записанные в такой программе по отдельности, независимо друг от друга (вычисление значений отдельных элементов списка, полей кортежа и т.и.), могут вычисляться в любой последовательности или даже параллельно. При наличии нескольких независимых процессоров, работающих в общей памяти, вычисления, происходящие по программе, составленной только из вызовов чистых функций, легко распараллелить. Именно это свойство функционального стиля программирования привлекает к нему все возрастающий интерес.
Можно ли писать программы в функциональном стиле на традиционном языке программирования? Конечно, да. Если программа представляет собой набор чистых детерминированных функций, то она будет «функциональной» независимо от того, написана ли она па специальном языке функционального программирования Haskell или на традиционном Java. Рассмотрим, например, задачу вычисления суммы вещественных элементов списка. Функция, решающая эту задачу, должна, получив в качестве исходных данных список из вещественных чисел, вычислить и выдать в качестве результата сумму элементов списка. На языке Java функция может выглядеть следующим образом (листинг 1.1).
Листинг 1.1. Функция вычисления суммы элементов числового списка
double sumList(List list) < double sum = 0;
Эта функция, конечно же, детерминированная и «чистая», она выдает всегда один и тот же результат па одинаковых входных данных и не влияет на поведение других функций. Однако все же с точки зрения функционального стиля она имеет одну «неправильность». Дело в том, что в функции определяется и используется локальная переменная sum, которая нужна для запоминания промежуточных значений суммы. В данном случае это никак не влияет на «чистоту» написанной функции, но то, что переменная изменяет значения по ходу работы, означает, что последовательность выполнения действий имеет существенное значение. «Распараллелить» работу такой функции очень трудно, если не невозможно. Во всяком случае, для этого требуется серьезный анализ смысла производимых действий. Мы видим, что присваивание переменным новых значений приводит к тому, что существенное значение начинает иметь последовательность выполнения действий.
В чисто функциональных программах присваиваний вообще нет, как нет и понятия последовательного вычисления. Каждая функция должна представлять собой суперпозицию обращений к другим функциям. В нашем же примере порядок вычислений задается строго, в нем предписывается, что сначала требуется присвоить переменной sum нулевое значение, а потом последовательно увеличивать его на величину значения каждого элемента списка.
Впрочем, в нашем случае исправить программу, приведя ее в соответствие с принципами функционального стиля программирования, можно. Для этого определим нашу функцию следующим образом (листинг 1.2).
Листинг 1.2. Рекурсивная функция вычисления суммы элементов списка
size == 1 ? list.get(O) :
sumList(list.subList(0, mid)) + sumList(list.subList(mid, size));
Эта программа уже действительно чисто функциональная. В ней нет переменных (иными словами, описаны идентификаторы size и mid, но это на самом деле не переменные, а константы, что дополнительно подчеркнуто использованием ключевого слова final). Обратите внимание также и на то, что вместо цикла использована рекурсия, а вместо условного оператора мы в этой программе применяем условные выражения, которые соединяют условиями не отдельные части последовательно выполняющейся программы, а отдельные подвыражения. Это тоже является характерной особенностью функционального стиля программирования.
Теперь программа абсолютно не зависит от порядка вычислений (если не считать того, что есть зависимость одних вычисляемых значений от других). Фактически эта программа может управляться не тем, в какой последовательности предписано выполнять действия, а тем, готовы ли данные для того, чтобы с ними можно было выполнить те или иные вычисления. Правда, убрав возможность присваивания значений переменным, мы тем самым сделали бессмысленным использование и одного из самых мощных средств традиционного программирования — циклов. Действительно, циклы имеют смысл только в том случае, если при каждом исполнении тела цикла внешняя среда (совокупность значений доступных в данный момент переменных) хотя бы немного, но изменяется. В противном случае цикл либо не исполняется ни разу, либо будет продолжать выполняться бесконечно.
Приведенная в листинге 1.2 функция абсолютно неестественна для традиционного программирования, однако она может дать значительное увеличение скорости работы в условиях многопроцессорного компьютера.
Может быть, для того чтобы писать в функциональном стиле, нужно просто взять привычный язык, скажем, тот же язык Java, и «выкинуть» из него переменные, присваивания и циклы, вообще любые «последовательные» операторы? Конечно, в результате действительно получится язык, на котором программы будут всегда написаны «в функциональном стиле», но получившийся язык будет слишком бедным для того, чтобы на нем можно было писать действительно полезные программы. В «настоящих» языках функционального программирования с функциями можно работать значительно более гибко, чем позволяется в традиционных языках программирования.
Рассмотрим следующую простую задачу. Пусть требуется определить функцию, с помощью которой можно создавать суперпозицию двух вещественных функций одного вещественного аргумента. Другими словами, нужно описать инструмент, с помощью которого из двух произвольных функций f (х) и g (х) можно было бы получить новую функцию fg (х), результат работы которой определялся бы уравнением fg(x) = f (g (х) ). Конечно, решение этой задачи было бы очень полезным в ситуации, когда практически единственным инструментом программирования является определение и вызов различных функций. В последних версиях языка Java, начиная с версии Java 8, имеется довольно широкий набор средств функционального программирования, так что поставленную задачу можно решить довольно просто (функция образования суперпозиции двух других функций включена в стандартную библиотеку программ этой версии Java). В листинге 1.3 приведена искомая функция.
Листинг 1.3. Реализация суперпозиции на Java static FunctiorKT,V>
compose (FunctionctJ, V> f, FunctiorKT, U> g) < return x ->f.apply(g.apply (x));
Даже если вы не очень хорошо разбираетесь во всех деталях записи, общий смысл написанного понять можно. Результатом работы этой функции является новая функция с аргументом х, результат работы которой — последовательное применение (apply) функций g и f к этому аргументу. Видно, впрочем, что на самом деле аргументы f ид — это не совсем функции: запись f (д (х) ) синтаксически неверна. Объекты типа Function — это сложно устроенные объекты, внутри которых и спрятана исполняемая функция. Несмотря на внешнюю простоту записи, в приведенной реализации имеется много скрытых деталей, которые необходимы для того, чтобы весьма сложно организованный объект имел вид обычной функции. Например, если подумать, то становится непонятно, почему аргументы функции comp — переданные ей функции f и g — не пропадают после выхода из этой функции, а остаются «жить» внутри возвращаемого результата.
Немного странным выглядит и использование функции compose в довольно простой ситуации. Естественная на первый взгляд запись compose (Math. sin, Math.cos) (3.14) оказывается синтаксически неверной. Правильной будет запись compose (Math: :sin, Math: :cos) . apply (3.14). Очень хорошо видно, что используемые нами «функции» — это не совсем функции в традиционном понимании, и в язык Java пришлось добавить много нового синтаксиса для того, чтобы выразить такие манипуляции с функциями, которые в функциональных языках выглядят очень просто и естественно, да и реализуются просто.
Причина описанных сложностей в выражении простых манипуляций с функциями заложена в структуре традиционных языков, рассчитанных на последовательное исполнение шагов алгоритма, очень глубоко, и для того чтобы можно было легко записывать программы для решения задач вроде только что описанной, требуется определять языки с совершенно другими свойствами. Этой цели, в частности, и служат специализированные языки функционального программирования. В них функции являются значениями «первого класса», т.е. с ними можно проделывать все то же, что и с другими «обычными» значениями. Над функциями можно выполнять операции и применять к ним другие функции как к аргументам; можно написать функцию, результатом работы которой будет также функция; функции могут быть элементами сложных структур данных и т.п.
Основной итог материала данной главы состоит в том, что функциональное программирование, как и другие парадигмы (объектно-ориентированное, логическое и др.), имеет свою область применимости и свои преимущества. Эти преимущества позволяют отдавать предпочтение использованию функциональных языков программирования для решения задач в определенной области, в первую очередь, для повышения быстродействия в условиях многопроцессорных комплексов и распределенных вычислений. В дальнейшем мы увидим, что функциональные языки программирования позволяют также во многих случаях получать чрезвычайно короткие и красивые представления многих алгоритмов, для описания которых традиционным способом требуется многословное и не всегда интуитивно понятное описание.