Программирование базовых функций
Основные принципы программирования: функциональное программирование
- Переводы, 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#.
Функциональное программирование
В первом случае основные принципы понятны: вы оперируете математической логикой для вывода новых фактов и состояний из уже известных. Ярким примером такого языка является Prolog.
Принцип работы с императивным программированием, наиболее распространённым, заключается в формировании инструкций, последовательных команд, которые должна выполнять машина. За примерами далеко ходить не надо, просто откройте список самых популярных языков программирования: те, что сверху — императивные.
А вот функциональное программирование – это то, что понять после классической школьной программы уроков информатики бывает сложно. Именно поэтому для общего развития и возможного будущего опыта вот наиболее полезные ресурсы по функциональному программированию.
Что это
Итак, как мы уже выяснили, императивное программирование работает со строго определёнными состояниями и инструкциями. Функциональное же основывается на взаимодействии с функциями, то есть некими процессами, описывающими связь между входными и выходными параметрами. Таким образом, в то время, как императивный язык описывает конкретное действие с известными входными параметрами, функциональный описывает некое тело взаимодействий, не опускаясь до конкретных случаев.
Функциональное программирование, несмотря на кажущуюся сложность, несёт в себе ряд преимуществ:
- Код становится короче;
- Понятнее;
- Включает в себя признаки хороших императивных языков: модульность, типизация, чистота кода.
Примерами функциональных языков являются LISP (Clojure), Haskell, Scala, R. В общем-то, вы даже можете попробовать писать функциональный код на Python или Ruby, но это больше развлечение для мозгов, нежели рациональное использование возможностей языка.
Конкретнее
Логично, что по функциональному программированию, существующему уже почти 50 лет, написано множество книг и статей. Поэтому какой смысл представлять собственную версию «ФП для чайников», если всё уже в прекрасном и удобочитаемом виде давно есть в сети? Поэтому просто поделимся ссылками:
- Прекрасная статья, имеющая исторический экскурс, яркие образы, но главное хорошие примеры. Имеется перевод.
- Книга, которую необходимо прочитать каждому функциональщику, если можно так выразиться. Тоже есть на русском.
- Онлайн-курс, который можно прослушать на английском языке. Будем надеяться, что-то похожее скоро появится и у нас на GeekBrains.
- Забавное и познавательное слад-шоу на тему функционального программирования.
- Прекрасная книга про Haskell, написанная доступным языком (русским), для тех, кто созрел для полноценного изучения первого функционального языка. Справочник прилагается.
- Для тех, кто предпочитает начать изучение не с простого, а с хронологического начала – перевод книги Кристиана Кеннека «Les Langages Lisp». Она же «Lisp in Small Pieces».
Куда с этими знаниями идти
Что касается области применения, то функциональное программирование является незаменимым инструментом при создании искусственного интеллекта или в тех областях, где императивные языки потребляют слишком много ресурсов (например, в Data Science). Так что если решили направить свою дальнейшую карьеру в это русло, то самое время обложиться описанной выше литературой и оставить свой след в чьей-то виртуальной голове.
Если вы новичок в мире программирования, то возможно ещё не знаете, что существуют три основных парадигмы: логическое программирование, императивное и функциональное.
В первом случае основные принципы понятны: вы оперируете математической логикой для вывода новых фактов и состояний из уже известных. Ярким примером такого языка является Prolog.
Принцип работы с императивным программированием, наиболее распространённым, заключается в формировании инструкций, последовательных команд, которые должна выполнять машина. За примерами далеко ходить не надо, просто откройте список самых популярных языков программирования: те, что сверху — императивные.
А вот функциональное программирование – это то, что понять после классической школьной программы уроков информатики бывает сложно. Именно поэтому для общего развития и возможного будущего опыта вот наиболее полезные ресурсы по функциональному программированию.
Что это
Итак, как мы уже выяснили, императивное программирование работает со строго определёнными состояниями и инструкциями. Функциональное же основывается на взаимодействии с функциями, то есть некими процессами, описывающими связь между входными и выходными параметрами. Таким образом, в то время, как императивный язык описывает конкретное действие с известными входными параметрами, функциональный описывает некое тело взаимодействий, не опускаясь до конкретных случаев.
Функциональное программирование, несмотря на кажущуюся сложность, несёт в себе ряд преимуществ:
- Код становится короче;
- Понятнее;
- Включает в себя признаки хороших императивных языков: модульность, типизация, чистота кода.
Примерами функциональных языков являются LISP (Clojure), Haskell, Scala, R. В общем-то, вы даже можете попробовать писать функциональный код на Python или Ruby, но это больше развлечение для мозгов, нежели рациональное использование возможностей языка.
Конкретнее
Логично, что по функциональному программированию, существующему уже почти 50 лет, написано множество книг и статей. Поэтому какой смысл представлять собственную версию «ФП для чайников», если всё уже в прекрасном и удобочитаемом виде давно есть в сети? Поэтому просто поделимся ссылками:
- Прекрасная статья, имеющая исторический экскурс, яркие образы, но главное хорошие примеры. Имеется перевод.
- Книга, которую необходимо прочитать каждому функциональщику, если можно так выразиться. Тоже есть на русском.
- Онлайн-курс, который можно прослушать на английском языке. Будем надеяться, что-то похожее скоро появится и у нас на GeekBrains.
- Забавное и познавательное слад-шоу на тему функционального программирования.
- Прекрасная книга про Haskell, написанная доступным языком (русским), для тех, кто созрел для полноценного изучения первого функционального языка. Справочник прилагается.
- Для тех, кто предпочитает начать изучение не с простого, а с хронологического начала – перевод книги Кристиана Кеннека «Les Langages Lisp». Она же «Lisp in Small Pieces».
Куда с этими знаниями идти
Что касается области применения, то функциональное программирование является незаменимым инструментом при создании искусственного интеллекта или в тех областях, где императивные языки потребляют слишком много ресурсов (например, в Data Science). Так что если решили направить свою дальнейшую карьеру в это русло, то самое время обложиться описанной выше литературой и оставить свой след в чьей-то виртуальной голове.
Итак, вы хотите научиться функциональному программированию (Часть 1)
Первый шаг к пониманию идей функционального программирования — самый важный и иногда самый сложный шаг. Но с правильным подходом никаких трудностей быть не должно.
Обучение вождению
Когда мы только учились водить машину, мы старались изо всех сил. Конечно, это выглядело легко, когда мы смотрели, как водят другие люди. Но на деле всё оказывалось сложнее.
Мы упражнялись на машине родителей и не выезжали на автострады, пока в совершенстве не осваивали улицы родного района.
После множества практических занятий и нескольких щекотливых моментов, о которых наши родители хотели бы забыть, мы выучивались вождению и наконец получали свои водительские права.
С правами в руках мы садились за руль при любом удобном случае. С каждой новой поездкой наши навыки становились всё лучше и лучше, уверенность росла. Затем наступал день, когда нам приходилось вести другую машину или наша собственная приказывала долго жить, и мы покупали новую.
Вспомните, на что был похож первый раз за рулём другой машины? Было ли это похоже на самый первый раз за рулём машины вообще? Даже не близко. В первый раз всё было таким незнакомым. Конечно, мы сидели до этого в машине, но только в роли пассажира. На этот раз мы оказывались в сидении водителя. Один на один со всеми рычагами, кнопками и педалями.
Но когда мы вели нашу вторую машину, мы просто задавали себе несколько простых вопросов по типу: куда вставляется ключ, где переключаются фары ближнего и дальнего света, как использовать поворотники и как настроить зеркала заднего вида.
После всего этого мы вели свою машину как по маслу. Но почему в этот раз всё было так просто по сравнению с первым разом?
Потому что новая машина была достаточно похожа на старую. Она имела все те базовые элементы, что нужны машине, и в большинстве случаев они находились на тех же местах, что и в старой.
Может, несколько вещей были реализованы как-то иначе и, может быть, они имели какие-то дополнительные функциональные возможности, но мы и так не использовали их во всём нашем водительском опыте. Рано или поздно мы изучали всё новые примочки. Как минимум, те, что нам реально требовались.
Что ж, процесс изучения языков программирования похож на процесс обучения вождению. Первый раз — самый сложный. Но с багажом опыта за плечами всё последующее обучение становится проще.
Когда вы начинаете изучать второй язык, вы спрашиваете себя: “Как мне создать модуль? Как реализовать поиск по массиву? Какие параметры принимает функция нахождения подстроки?”.
Вы уверены, что можете «научиться водить» на этом новом языке, потому что он напоминает вам о предыдущем языке, может, с несколькими новыми элементами, которые, надо надеяться, сделают вашу жизнь легче.
Ваш первый космический корабль
Ездили ли вы на одном автомобиле всю свою жизнь или на десятках автомобилях, представьте, что собираетесь сесть за штурвал космического корабля.
Если вы собираетесь летать на таком аппарате, то вряд ли будете надеяться, что навыки вождения на дороге как-то особенно вам помогут. Вы начнёте всё с нуля (Мы же программисты, чёрт возьми. Мы начинаем считать с нуля.).
Вы начнёте свои тренировки с расчётом, что в космосе всё работает иначе и что полёты на этой штуковине довольно отличны от вождения машины по земле.
Однако физика не изменилась. Путь, по которому вы двигаетесь, находится в пределах всё той же Вселенной.
Такой же подход должен быть к изучению функционального программирования. Вы должны учитывать, что всё будет по-другому. И то многое, что вы знали о программировании не будет перенесено в новую область.
Программировать — значит мыслить и функциональное программирование научит вас мыслить совсем по-другому настолько, что вы вероятно никогда не вернётесь назад на старый путь образа мышления.
Забудьте всё, что вы знаете
Люди любят говорить эту фразу и в ней действительно есть доля правды. Изучать функциональное программированию — значит учить всё с нуля. Не полностью, конечно, но фактически это так. В этой теме существует множество простых концепций, но вам лучше приготовиться к тому, что придётся переучивать всё.
С правильным подходом у вас будут правильные ожидания, а с правильными ожиданиями вы не захотите бросить дело, когда начнутся вещи по-тяжелее.
Есть также многие вещи, которые вы привыкли делать, как программист, но которые вы не сможете больше делать, занимаясь функциональным программированием.
Вспомните, как вы, чтобы выехать с проезжей части, давали задний ход на машине. Но на космическом корабле нет механизма реверса. Теперь вы должны подумать: “ЧТО? НЕТ ЗАДНЕГО ХОДА? КАК Я ДОЛЖЕН ВОДИТЬ БЕЗ ЗАДНЕГО ХОДА?”.
Что ж, оказывается вам не нужен реверс на космическом корабле, способном маневрировать в трёхмерном пространстве космоса. Как только вы поймёте это, вы больше не будете вспоминать о возможности заднего хода. И однажды, вы даже задумаетесь о том, насколько, на самом деле, ограничены обыкновенные машины.
Изучение функционального программирования требует времени. Запаситесь терпением.
Так что давайте покинем холодный мир императивного и медленно окунёмся в горячие источники функционального программирования.
То, что следует в этой комплексной статье, — концепции функционального программирования, которые помогут вам перед полным погружением в первый функциональный язык. Или, если вы уже сделали решительный шаг в этой сфере, эти параграфы помогут заточить понимание идей.
Пожалуйста, не спешите. С этого момента не торопитесь и находите время, чтобы понять примеры кода. Лучше будет даже сделать небольшую паузу в прочтении после этой части статьи и дать озвученным идеям “устаканиться”. Затем возвращайтесь к чтению.
Самое главное — это то, чтобы вы поняли.
Чистота
Если говорится о чистоте в функциональном программировании, значит подразумеваются чистые функции.
Чистые функции — очень простые. Они всего лишь производят операция над входными данными.
Вот пример чистой функции:
Заметьте, что функция add не прикасается к переменной z . Она не читает её значения и ничего не пишет в неё. Функция читает только x и y , свои входные данные, и возвращает результат их суммы.
Это и есть чистая функция. Если функция add имеет доступ к переменной z , она больше не может быть чистой.
Это пример другой чистой функции:
Если функция justTen чистая, она может возвращать только значение-константу. Почему?
Потому что мы не даём ей никаких входных данных. А значит, чтобы быть чистой, она не должна изменять никаких переменных, кроме тех, что были ей переданы. Единственное, что может возвратить такая функция — константа.
Пока функции, не принимающие параметров, не работают, они не очень полезны. Было бы лучше объявить justTen просто как константу.
Более полезные чистые функции принимают хотя бы один параметр.
Взгляните на этот пример:
Посмотрите, эта функция ничего не возвращает. Она складывает x и y , записывает результат в переменную z , но не возвращает её.
Эта чистая функция работает только с входными данными. Да, она выполняет сложение, но пока обратно возвращается ничего, функция бесполезна.
Все полезные чистые функции должны возвращать что-нибудь.
Давайте рассмотрим пример с первой функцией add ещё раз:
Обратите внимание, что add(1, 2) в результате всегда даёт 3 . Конечно, сюрприз не большой, но это потому что функция чистая. Если бы функция add брала значение откуда-то снаружи, вы бы никогда не могли наверняка предсказать её поведение.
Чистая функция всегда возвращает одинаковые значения для одинаковых входных данных.
Поскольку чистые функции не могут изменять внешние переменные, все эти функции являются нечистыми:
Все функции в примере имеют то, что называют побочными эффектами. Когда Вы вызываете их, они меняют файлы и таблицы баз данных, отправляют данные на сервер или обращаются к операционной системе, чтобы получить сокет. Они делают куда больше, чем просто оперирование входными данными и возвращение значений. Следовательно, вы никогда не можете предсказать, что функция возвратит.
Чистые функции не имеют побочных эффектов.
В таких языках императивного программирования как JavaScript, Java и C# побочные эффекты везде. Это делает отладку проблематичной, потому что в коде Вашей программы переменная может быть изменена где угодно. В общем, если у Вас баг из-за переменой, принявшей неверное значение в неподходящее время, где Вы будете искать ошибку? Везде? Так дело не пойдёт.
На этом месте, вы, вероятно, думаете: “КАК, ЧЁРТ ПОБЕРИ, Я СДЕЛАЮ ХОТЬ ЧТО-НИБУДЬ ОДНИМИ ТОЛЬКО ЧИСТЫМИ ФУНКЦИЯМИ?”.
В функциональном программировании вы не пишите только чистые функции.
Функциональные языки не могут исключить побочных эффектов, они могут только изолировать их. Пока у программ будут интерфейсы, взаимодействующие с реальным миром, некоторые части любой программы должны быть нечистыми. Цель — это свести к минимуму количество нечистого кода и отделить его от остальной части программы.
Неизменяемость
Вы помните, когда впервые увидели следующий код:
И тот, кто учил вас программированию, говорил забыть изученное на уроках математики. Ведь в математике x никогда не мог равняться x + 1 .
Но в императивном программировании данный код означает «взять текущее значение x , прибавить к нему 1 , положить результат обратно в x ».
Что ж, в функциональном программировании выражение x = x + 1 недопустимо. Так что Вам надо вспомнить то, что вы забыли из математики. Если так можно выразиться.
В функциональном программировании нет переменных.
Сохранённые значения всё ещё называются переменными по историческим причинам, но они являются константами, то есть x, однажды приняв какое-либо значение, сохраняет его на всю жизнь.
Не волнуйтесь, x – это обычно локальная переменная, так что её жизнь достаточно коротка. Но пока она жива, она никак не изменится.
Вот пример переменной-константы в Elm — чистом языке функционального программирования для web-разработки:
Если вы не знакомы с синтаксисом семейства языков программирования ML, позвольте мне объяснить. addOneToSum – это функция, принимающая 2 параметра: y и z .
Внутри блока let x приписывается значение 1 , то есть он равен 1 до конца своей жизни. Его жизнь кончается, когда происходит выход из функции, или, более точно, когда исполняется блок let .
Внутри блока in вычисления могут включать значения, объявленные в блоке let , а именно: x . Возвращается результат вычисления x + y + z или, в точности, возвращается 1 + y + z , так как x = 1 .
И снова я могу услышать, как вы вопрошаете: “КАК, ЧЕРТ ПОБЕРИ, Я ДОЛЖЕН СДЕЛАТЬ ХОТЬ ЧТО-НИБУДЬ БЕЗ ПЕРЕМЕННЫХ?!”.
Давайте подумаем, когда обычно мы хотим изменить переменную. Всего две основных причины, которые приходят на ум: многозначные изменения (например, изменение отдельного значения объекта или записи) и однозначные изменения (например, счётчики цикла).
Функциональное программирование решает проблему изменений значения записи, делая копию уже изменённой записи. Это происходит оперативно, без копирования всех частей записи, используя определённые структуры данных, делающие это возможным.
Функциональное программирование решает также проблему однозначных изменений переменных, в сущности, тем же путём, просто делая их копию.
Да, кстати, и всё это без циклов.
“СНАЧАЛА БЕЗ ПЕРЕМЕННЫХ, А ТЕПЕРЬ ЕЩЁ И БЕЗ ЦИКЛОВ? Я ТЕБЯ НЕНАВИЖУ. ”
Попридержите коней. Это не значит, что мы не можем использовать циклы, просто здесь нет таких характерных операторов как for , while , do , repeat и так далее.
Функциональное программирование использует рекурсию для выполнения цикла.
Вот два примера реализации цикла в JavaScript.
Обратите внимание, как рекурсия в функциональном подходе осуществляет то же самое, что и оператор цикла for , вызывая саму себя с новым параметром запуска (start + 1) и с новым счетчиком (acc + start) . Она не изменяет старых значений. Вместо этого она использует новые значения, высчитанные из старых.
К сожалению, такие примеры не очевидны в JavaScript (даже если вы потратили некоторое время на их изучение) по двум причинам. Во-первых, синтаксис JavaScript засорён, а во-вторых, вы, вероятно, не привыкли думать рекурсивно.
Пример на языке Elm читать и, следовательно, понимать легче:
Так этот код выполняется:
Вам скорее всего кажется, что циклы for гораздо легче для понимания. Хотя это спорно и скорее всего является вопросом осведомлённости, не-рекурсивные циклы подразумевают изменчивость, что по своей сути плохо.
Я не объясняю здесь преимущества использования парадигмы неизменяемости, но вы можете посмотреть параграф под названием Global Mutable State в статье Why Programmers Need Limits, если хотите изучить эту тему.
Одно очевидное преимущество — то, что если вы имеете доступ к какому-либо значению в вашей программе, это доступ только для чтения, а значит никто другой не может изменить это значение. Даже вы сами. Вследствие, никаких случайных изменений.
Также, если программа многопоточная, исполнение никакого потока не разрушит ваши планы. Поскольку значение — константа и если поток захочет изменить его, ему придётся создать новое значение из старого.
Ещё в середине девяностых я написал игровой движок для Creator Crunch и самый большой источник ошибок был связан с вопросом многопоточности. Я хотел бы знать про неизменяемость в то время. Но тогда меня больше волновала разница между двух и четырёх скоростными приводами CD-ROM при игре.
Неизменяемость делает код проще и безопаснее.
Мой мозг.
Пока что достаточно.
В последующих частях этой статьи я расскажу про функции более высокого порядка, функциональную композицию, каррирование и ещё много о чём.
Знакомьтесь: функциональное программирование
Всяческих концепций, идей и парадигм в программировании много. И это, в общем-то, хорошо, потому что каждый может выбрать ту, которая наилучшим образом сочетается с его внутренним видением программирования и с решаемой в данном случае задачей. Сейчас я познакомлю вас с одним из самых интересных, на мой взгляд, подходов к написанию программ, который наверняка будет особенно интересен тем из читателей, кто пока с ним не сталкивался.
“Подобно ООП, функциональное программирование – это множество идей, но не множество жёстких правил”
Чем плохи большинство статей по функциональному программированию, так это тем, что они весьма далеки от проблем простого программиста, которого повышение собственной зарплаты (к счастью) волнует гораздо больше, чем различные выкладки из области дискретной математики, которыми больны такие статьи. Поэтому я постараюсь не наступать на эти распространённые грабли – думаю, это получится, потому что я не математик.
Итак, какая же основная мысль лежит в основе парадигмы функционального программирования? Давайте повнимательнее присмотримся к самому названию. Не нужно быть филологом, чтобы заметить, что слово “функциональное” явно связано со словом “функция”. В функциональном программировании функции понимаются в своём изначальном, математическом смысле – это некоторый набор операций, принимающих входное и возвращающих определённое значение каких-либо переменных. Чем это, спросите вы, отличается от обычного программирования, не суть важно – процедурного или объектно-ориентированного? Вопрос резонный, ибо в том изложении, в котором представил основную идею функционального программирования я, действительно сложно заметить какое-либо его отличие от традиционных концепций программирования.
Основное отличие состоит в том, что функции, как и функции математические, не производят никаких побочных действий. Они только принимают входные значения, крутят и вертят их так, как им заблагорассудится, а после выдают на-гора результат, радующий или не радующий программиста. При этом они не изменяют никаких флагов состояния, не записывают никакой информации в глобальные переменные, не пытаются освободить занятую другими объектами память. Видите, какое сразу вырисовывается преимущество: если программа состоит исключительно из функций, работающих только с переданными им переменными, то никакая функция не сможет вызвать ошибок доступа к памяти – а ведь именно такие ошибки наиболее противные в плане локализации их источников. Кроме того, становится не столь важным соблюдение последовательности выполнения операторов в программе – каждая функция может вычислить своё значение в любой момент, что позволяет легко распараллеливать программы, написанные в функциональном стиле.
Но, пожалуй, самое важное преимущество программ, написанных в функциональном стиле – это, конечно же, их логическая связность и краткость. Программы на функциональных языках программирования могут быть в десятки раз короче тех же самых программ, написанных на традиционных языках. Когда я говорю о традиционных языках здесь, то подразумеваю под ними императивные языки программирования – то есть такие, в которых для выполнения некой операции нужно пошагово описать её выполнение. Примеры таких языков – знакомые всем Basic, Pascal, C.
“В сравнении с пользователями С, “никто” – достаточно точная оценка числа пользователей функциональных языков”
Сразу после рассказа о бочке мёда я расскажу о ложке дёгтя. Действительно, как же так получается, что функциональным программированием, которое решает огромное количество наболевших программистских проблем, пользуются, мягко скажем, не все программисты? Тому есть множество причин. Часть из них я почерпнул из замечательной статьи Филипа Вадлера “Почему никто не использует функциональные языки?” (её можно найти по адресу www.softcraft.ru/paradigm/fp/whynotfp.shtml). Главными недостатками функциональных языков программирования он считает плохую совместимость с самыми популярными из императивных языков программирования, на которых сейчас написано подавляющее большинство широко используемого программного обеспечения, плохую переносимость программ на функциональных языках на различные платформы и низкую популярность этих языков (то есть, получается рекурсия – языки не популярны из-за того, что все опасаются серьёзно их использовать из-за их низкой популярности). Есть ещё ряд причин, но все они также, в большинстве своём, упираются в инерционность индустрии программного обеспечения, в том числе и в инерционность людей, которые работают в ней.
Тем не менее, тенденции теперь таковы, что функциональному программированию прочат блестящее будущее и популярность не меньшую, чем у программирования объектно-ориентированного. Всё-таки винторогих баранов, кричащих “Нам нужен только Си/Java/Ассеблер” (нужное подчеркнуть), среди разработчиков софта всё же меньше, чем здравомыслящих людей. А потому языки функционального программирования постепенно приживаются в тех нишах, для которых подходят наилучшим образом – и это вполне естественно, так как язык программирования, как и любой другой нишевый продукт, не может при всех своих реальных или мнимых достоинствах сразу захватить значительную часть рынка. Поэтому будьте терпеливыми: изучение функционального программирования – это такое вложение времени, которое не отразится сразу на вашей зарплате, зато позволит в будущем оставаться востребованным и высокооплачиваемым специалистом.
“Функциональный программист смотрится как средневековый монах, отвергающий удовольствия жизни в надежде, что это сделает его добродетельным. Для тех, кто заинтересован в материальных выгодах, эти “преимущества” не очень убедительны. (. ) Ясно, что такая характеристика функционального программирования неадекватна.”
Самое вкусное я постарался оставить на потом. Вы обращали внимание, что на сборных концертах в финале всегда выступают самые именитые звёзды, а на сольных исполнители поют именно проверенные временем хиты, а не самые новые песни? Вот и в этой статье самые привлекательные особенности функционального программирования откроются самым стойким из читателей – тем, кто не бросил читать, а дочитал досюда.
Итак, первая “вкусность”. В функциональном программировании это называется функциями высших порядков, в математическом анализе (или, если более точно, то в вариационном исчислении) – функционалами. Это функции, аргументами для которых могут быть другие функции. Не значения, возвращаемые этими функциями, – нет-нет, именно сами функции. Простейший функционал – это производная, другой пример – первообразная. Фактически, функции высших порядков – это некоторый аналог того, как объектно-ориентированные языки программирования позволяют обращаться программисту с классами. Функции высшего порядка позволяют очень просто масштабировать приложения. Например, при загрузке данных разного формата вы можете передавать соответствующую функцию загрузки базовой функции, которая сама вызовет нужную в зависимости от вида данных. Если придётся добавить ещё один формат, вы просто добавите ещё одну функцию, не изменяя при этом кода, отвечающего за логику приложения.
Теперь ещё одна возможность, которая, вероятно, покажется вам даже более интересной, так как её аналоги напрочь отсутствуют в императивных языках. Это так называемые ленивые вычисления, называемые также отложенными. Ленится при этом, правда, не программист (программисты и так существа невероятно ленивые в своём подавляющем большинстве), а сама программа. Что это значит? Это значит, что она откладывает все вычисления до тех пор, пока не понадобится их результат. Программист только описывает зависимости функций друг от друга (не забывая при этом, конечно же, использовать такую замечательную вещь, как функции высших порядков), а программа уже сама будет решать, когда вычислять значение той или иной функции. Таким образом, увеличивается производительность программы и, опять-таки, улучшается потенциальная возможность её грамотно распараллелить. Впрочем, ленивые вычисления поддерживаются не всеми функциональными языками (но самый популярный из них, Haskell, их поддерживает) и при этом поддерживаются многими языками программирования, не относящимися к функциональным (например, Python – но там они организованы несколько иначе).
Ещё одна любопытная деталь языков функционального программирования – это так называемый карринг. Что это такое? Оказывается, несмотря на несколько аляповатое и устрашающее непосвящённых название, всё очень и очень просто. Это способность функциональных языков программирования быстро создавать функции, являющиеся обёртками для других функций, не заставляя при этом программиста писать рутинный вспомогательный код. В объектно-ориентированных и процедурных (сиречь императивных) языках программирования для того, чтобы определить обёртку для какой-либо функции или метода, программист должен вручную прописывать все определения новой функции. Функциональные языки (не берусь, впрочем, утверждать, что все – их много, очень много) позволяют гораздо быстрее определить новую функцию, если нужно уменьшить число параметров оборачиваемой и заменить часть из них на заранее предопределённые константы.
В общем-то, этими тремя пунктами интересные и мощные особенности функциональных языков программирования далеко не кончаются – тема эта благодатная, и говорить о ней можно куда дольше, чем мне позволит главный редактор. Поэтому, думаю, пора сворачиваться и подводить итоги нашего с вами разговора о функциональном программировании.
Во-первых, функциональное программирование – мощная парадигма, позволяющая устранить многие минусы объектно-ориентированного программирования (ну и при этом наломать новых дров – куда же без этого?). Хотя сейчас парадигма эта и не очень популярна в индустрии программного обеспечения, тем не менее, у неё есть все шансы стать куда более распространённой, чем сейчас. Во-вторых, функциональное программирование уже сегодня предстаёт в виде множества специально разработанных под эту парадигму языков, которые позволяют программисту делать многие такие вещи, которые вовсе недоступны в императивных языках. Ну, а в-третьих, подходы, которые предлагает функциональная парадигма, можно вполне успешно применять и в нефункциональных языках, делая благодаря им свой программный код лучше. Так что функциональное программирование, как видите, заслуживает того, чтобы им интересоваться и его изучать.
Элементы функционального программирования
Функции являются абстракциями, в которых детали реализации некоторого действия скрываются за отдельным именем. Хорошо написанный набор функций позволяет использовать их много раз. Стандартная библиотека Python содержит множество готовых и отлаженных функций, многие из которых достаточно универсальны, чтобы работать с широким спектром входных данных. Даже если некоторый участок кода не используется несколько раз, но по входным и выходным данным он достаточно автономен, его смело можно выделить в отдельную функцию.
Эта лекция более ориентирована на практические соображения, а не на теорию функционального программирования. Однако там, где нужно, будут употребляться и поясняться соответствующие термины.
Далее будут подробно рассмотрены описание и использование функций в Python , рекурсия , передача и возврат функций в качестве параметров, обработка последовательностей и итераторы , а также такое понятие как генератор . Будет продемонстрировано, что в Python функции являются объектами (и, значит, могут быть переданы в качестве параметров и возвращены в результате выполнения функций). Кроме того, речь пойдет о том, как можно реализовать некоторые механизмы функционального программирования, не имеющие в Python прямой синтаксической поддержки, но широко распространенные в языках функционального программирования.
Что такое функциональное программирование?
Функциональное программирование – это стиль программирования, использующий только композиции функций . Другими словами, это программирование в выражениях, а не в императивных командах.
Как отмечает Дэвид Мертц (David Mertz) в своей статье о функциональном программировании на Python , “функциональное программирование – программирование на функциональных языках ( LISP , ML, OCAML, Haskell, . )”, основными атрибутами которых являются:
- “Наличие функций первого класса” (функции наравне с другими объектами можно передавать внутрь функций).
- Рекурсия является основной управляющей структурой в программе.
- Обработка списков (последовательностей).
- Запрещение побочных эффектов у функций, что в первую очередь означает отсутствие присваивания (в “чистых” функциональных языках)
- Запрещение операторов, основной упор делается на выражения. Вместо операторов вся программа в идеале – одно выражение с сопутствующими определениями.
- Ключевой вопрос: что нужно вычислить, а не как.
- Использование функций более высоких порядков (функции над функциями над функциями).
Функциональная программа
В математике функция отображает объекты из одного множества ( множества определения функции ) в другое ( множество значений функции ). Математические функции (их называют чистыми ) “механически”, однозначно вычисляют результат по заданным аргументам. Чистые функции не должны хранить в себе какие-либо данные между двумя вызовами. Их можно представлять себе черными ящиками, о которых известно только то, что они делают, но совсем не важно, как.
Программы в функциональном стиле конструируются как композиция функций. При этом функции понимаются почти так же, как и в математике: они отображают одни объекты в другие. В программировании “чистые” функции – идеал, не всегда достижимый на практике. Практически полезные функции обычно имеют побочный эффект: сохраняют состояние между вызовами или меняют состояние других объектов. Например, без побочных эффектов невозможно представить себе функции ввода-вывода. Собственно, такие функции ради этих “эффектов” и используются. Кроме того, математические функции легко работают с объектами, требующими бесконечного объема информации (например, вещественные числа). В общем случае компьютерная программа может выполнить лишь приближенные вычисления.
Кстати, бинарные операции ” + “, ” – “, ” * “, ” / “, которые записываются в выражениях, являются “математическими” функциями над двумя аргументами — операндами. Их используют настолько часто, что синтаксис языка программирования имеет для них более короткую запись . Модуль operator позволяет представлять эти операции в функциональном стиле:
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.
Вы новичок в программировании?
Некоторые из вышеперечисленных терминов и языков могут показаться несколько эзотерическими, если вы еще не опытный кодер. Это хорошо, так как знание того, чего вы не знаете, является одним из первых шагов в становлении хорошего специалиста.
Некоторые из перечисленных выше языков лучше подходят для начинающих, чем другие. Посмотрите на наш список лучших языков программирования для начинающих .
Источники:
http://tproger.ru/translations/functional-programming-concepts/
http://geekbrains.ru/posts/functional_programming
http://medium.com/devschacht/charles-scalfani-so-you-want-to-be-a-functional-programmer-part-1-6ef98e90d58d
http://www.kv.by/archive/index2008241110.htm
http://www.intuit.ru/studies/courses/49/49/lecture/27062
http://www.make-info.com/5-functional-programming-languages-that-you-should-to-know/