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


Оглавление (нажмите, чтобы открыть):

Функциональное программирование

В первом случае основные принципы понятны: вы оперируете математической логикой для вывода новых фактов и состояний из уже известных. Ярким примером такого языка является Prolog.

Принцип работы с императивным программированием, наиболее распространённым, заключается в формировании инструкций, последовательных команд, которые должна выполнять машина. За примерами далеко ходить не надо, просто откройте список самых популярных языков программирования: те, что сверху — императивные.

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

Что это

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

Функциональное программирование, несмотря на кажущуюся сложность, несёт в себе ряд преимуществ:

  1. Код становится короче;
  2. Понятнее;
  3. Включает в себя признаки хороших императивных языков: модульность, типизация, чистота кода.

Примерами функциональных языков являются LISP (Clojure), Haskell, Scala, R. В общем-то, вы даже можете попробовать писать функциональный код на Python или Ruby, но это больше развлечение для мозгов, нежели рациональное использование возможностей языка.

Конкретнее

Логично, что по функциональному программированию, существующему уже почти 50 лет, написано множество книг и статей. Поэтому какой смысл представлять собственную версию «ФП для чайников», если всё уже в прекрасном и удобочитаемом виде давно есть в сети? Поэтому просто поделимся ссылками:

  1. Прекрасная статья, имеющая исторический экскурс, яркие образы, но главное хорошие примеры. Имеется перевод.
  2. Книга, которую необходимо прочитать каждому функциональщику, если можно так выразиться. Тоже есть на русском.
  3. Онлайн-курс, который можно прослушать на английском языке. Будем надеяться, что-то похожее скоро появится и у нас на GeekBrains.
  4. Забавное и познавательное слад-шоу на тему функционального программирования.
  5. Прекрасная книга про Haskell, написанная доступным языком (русским), для тех, кто созрел для полноценного изучения первого функционального языка. Справочник прилагается.
  6. Для тех, кто предпочитает начать изучение не с простого, а с хронологического начала – перевод книги Кристиана Кеннека «Les Langages Lisp». Она же «Lisp in Small Pieces».

Куда с этими знаниями идти

Что касается области применения, то функциональное программирование является незаменимым инструментом при создании искусственного интеллекта или в тех областях, где императивные языки потребляют слишком много ресурсов (например, в Data Science). Так что если решили направить свою дальнейшую карьеру в это русло, то самое время обложиться описанной выше литературой и оставить свой след в чьей-то виртуальной голове.

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

В первом случае основные принципы понятны: вы оперируете математической логикой для вывода новых фактов и состояний из уже известных. Ярким примером такого языка является Prolog.

Принцип работы с императивным программированием, наиболее распространённым, заключается в формировании инструкций, последовательных команд, которые должна выполнять машина. За примерами далеко ходить не надо, просто откройте список самых популярных языков программирования: те, что сверху — императивные.

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

Что это

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

Функциональное программирование, несмотря на кажущуюся сложность, несёт в себе ряд преимуществ:

  1. Код становится короче;
  2. Понятнее;
  3. Включает в себя признаки хороших императивных языков: модульность, типизация, чистота кода.

Примерами функциональных языков являются LISP (Clojure), Haskell, Scala, R. В общем-то, вы даже можете попробовать писать функциональный код на Python или Ruby, но это больше развлечение для мозгов, нежели рациональное использование возможностей языка.

Конкретнее

Логично, что по функциональному программированию, существующему уже почти 50 лет, написано множество книг и статей. Поэтому какой смысл представлять собственную версию «ФП для чайников», если всё уже в прекрасном и удобочитаемом виде давно есть в сети? Поэтому просто поделимся ссылками:

  1. Прекрасная статья, имеющая исторический экскурс, яркие образы, но главное хорошие примеры. Имеется перевод.
  2. Книга, которую необходимо прочитать каждому функциональщику, если можно так выразиться. Тоже есть на русском.
  3. Онлайн-курс, который можно прослушать на английском языке. Будем надеяться, что-то похожее скоро появится и у нас на GeekBrains.
  4. Забавное и познавательное слад-шоу на тему функционального программирования.
  5. Прекрасная книга про Haskell, написанная доступным языком (русским), для тех, кто созрел для полноценного изучения первого функционального языка. Справочник прилагается.
  6. Для тех, кто предпочитает начать изучение не с простого, а с хронологического начала – перевод книги Кристиана Кеннека «Les Langages Lisp». Она же «Lisp in Small Pieces».

Куда с этими знаниями идти

Что касается области применения, то функциональное программирование является незаменимым инструментом при создании искусственного интеллекта или в тех областях, где императивные языки потребляют слишком много ресурсов (например, в Data Science). Так что если решили направить свою дальнейшую карьеру в это русло, то самое время обложиться описанной выше литературой и оставить свой след в чьей-то виртуальной голове.

Элементы функционального программирования. Принципы функционального программирования: почему это важно

Язык функционального программирования

В качестве основных свойств функциональных языков программирования обычно рассматриваются [кем? ] следующие:

Программы на функциональных языках обычно намного короче и проще, чем те же самые программы на императивных языках.
Пример (быстрая сортировка Хоара на абстрактном функциональном языке) :

QuickSort () =
quickSort () = quickSort (n | n t, n h)

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

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

В функциональных языках (равно как и вообще в языках программирования и математике) функции могут быть переданы другим функциям в качестве аргумента или возвращены в качестве результата. Функции, принимающие функциональные аргументы, называются функциями высших порядков или функционалами.

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

В традиционных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. В каком-то смысле противоположностью вызова-по-значению является вызов-по-необходимости (ленивые вычисления). В этом случае аргумент вычисляется, только если он нужен для вычисления результата.

Некоторые языки функционального программирования

Классификация функциональных языков

В качестве примера чистого функционального языка можно привести Haskell . Однако большинство функциональных языков являются гибридными и содержат свойства как функциональных, так и императивных языков. Яркие примеры — языки Scala и Nemerle. В них органично сочетаются характеристики как объектно-ориентированных языков, так и функциональных. Реализована хвостовая рекурсия и её оптимизация, функция является полноправным объектом, то есть может быть сохранена в переменной, передана в качестве аргумента в другую функцию или возвращена из функции.

Также функциональные языки делят на строгие и нестрогие . К нестрогим языкам относят те, которые поддерживают отложенные вычисления (F#), то есть аргументы функции вычисляются только тогда, когда они действительно понадобятся при вычислении функции. Ярким примером нестрогого языка является Haskell. В качестве примера строгого языка можно привести Standard ML .

Некоторые функциональные языки реализованы поверх платформообразующих виртуальных машин (JVM, .NET), то есть приложения на этих языках могут работать в среде времени исполнения (JRE, CLR) и использовать встроенные классы. К ним относятся Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

Ссылки

  • https://fprog.ru/ — Журнал «Практика функционального программирования»
  • https://www.intuit.ru/department/pl/funcpl/1/ — Основы функционального программирования. Л. В. Городняя
  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года;
  • https://alexott.net/ru/fp/books/ — Обзор литературы о функциональном программировании . Рассматриваются книги как на русском, так и на английском языке.

Wikimedia Foundation . 2010 .

Смотреть что такое «Язык функционального программирования» в других словарях:

язык прграммирования Лисп — Язык функционального программирования. Тематики информационные технологии в целом EN Lisp … Справочник технического переводчика

Универсальный язык программирования высокого уровня. Язык Лисп: относится к декларативным языкам функционального типа; предназначен для обработки символьных данных, представленных в виде списков. Основой языка являются функции и рекурсивные… … Финансовый словарь

У этого термина существуют и другие значения, см. Alice. Alice Семантика: функциональный Тип исполнения: компиляция в байткод для виртуальной машины Появился в: 2002 … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Oz Семантика: функциональный, процедурный, декларативный, объектно ориентированный, вычисления с ограничениями, Н модели, параллельные вычисления Тип исполнения: компилируемый Появился в: 1991 Автор(ы): Gert Smolka his students Релиз … Википедия

AWL (Alternative Web Language) Класс языка: мультипарадигмальный: функциональный, процедурный, объектно ориентированный Тип исполнения: интерпретируемый Появился в: 2005 г. Типизация данных: динамическая … Википедия

У этого термина существуют и другие значения, см. Леда (значения). Леда (Leda) мультипарадигмальный язык программирования, спроектированный Тимоти Баддом. Язык Leda исходно создавался с целью совмещения императивного программирования, объектно… … Википедия

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

В языках функционального программирования основным конструктивным элементом является математическое понятие функции. Существует различия в понимании функции в математике и функции в программировании, в следствии чего нельзя отнести Си подобные… … Википедия

Python был задуман в 1980 х годах, а его создание началось в декабре 1989 года Гвидо ван Россумом в составе центра математики и информатики в Нидерландах. Язык Python был задуман как потомок языка программирования ABC, способный к обработке… … Википедия

Функциональное программирование объединяет разные подходы к определению процессов вычисления на основе достаточно строгих абстрактных понятий и методов символьной обработки данных.

Особенностью языков функционального программирования является то, что тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. Основные свойства функциональных языков программирования: краткость, простота, строгая типизация, модульность, наличие отложенных (ленивых) вычислений.

К функциональным языкам программирования относят: Lisp, Miranda, Gofel, ML, Standard ML, Objective CAML, F#, Scala, Пифагор и др.

Процедурные языки программирования

Процедурный язык программирования предоставляет возможность программисту определять каждый шаг в процессе решения задачи. Особенность таких языков программирования состоит в том, что задачи разбиваются на шаги и решаются шаг за шагом. Используя процедурный язык, программист определяет языковые конструкции для выполнения последовательности алгоритмических шагов.

Процедурные языки программирования: Ada, Basic, Си, КОБОЛ, Pascal, ПЛ/1, Рапира и др.

Стековые языки программирования

Стековый язык программирования − это язык программирования, в котором для передачи параметров используется машинная модель стека. Стековые языки программирования: Forth, PostScript, Java, C# и др. При использовании стека, в качестве основного канала передачи параметров между словами, элементы языка, естественным образом, образуют фразы (последовательное сцепление). Это свойство сближает данные языки с естественными языками.

Аспектно-ориентированные языки программирования 5) Декларативные языки программирования 6) Динамические языки программирования 7) Учебные языки программирования 8) Языки описания интерфейсов 9) Языки прототипного программирования 10) Объектно-ориентированные языки программирования 11) Логические языки программирования 12) Сценарные языки программирования 13) Эзотерические языки программирования

Стандартизация языков программирования. Парадигма программирования

Концепция языка программирования неотрывно связана с его реализацией. Для того чтобы компиляция одной и той же программы различными компиляторами всегда давала одинаковый результат, разрабатываются стандарты языков программирования. Организации, занимающиеся вопросами стандартизации: Американский национальный институт стандартов ANSI, Институт инженеров по электротехнике и электронике IEEE, Организация международных стандартов ISO.

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

Парадигма – набор теорий, стандартов и методов, которые совместно представляют собой способ организации научного знания, – иными словами, способ видения мира. По аналогии с этим принято считать, что парадигма в программировании – способ концептуализации, который определяет, как следует проводить вычисления, и как работа, выполняемая компьютером, должна быть структурирована и организована.

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

C и Pascal являются примерами языков, предназначенных для директивного программирования, когда разработчик программы использует процессно-ориентированная модель, то есть пытается создать код, должным образом воздействующий на данные. Активным началом при этом подходе считается программа (код), которая должна выполнить все необходимые для достижения нужного результата действия над пассивными данными.

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

Программирование – процесс создания компьютерных программ. В более широком смысле: спектр деят-сти, связ-ый с созданием и поддержанием в раб. состоянии программ — ПО ЭВМ.

Технология программирования — совокупность методов и средств, используемых в процессе разработки программного обеспечения.

Технология программир-я представляет собой набор технологических инструкций, включающих:

· указание последоват-сти выполнения технологич-х операций;

· перечисление условий, при кот-х выполняется та или иная операция;

· описания самих операций, где для каждой операции определены исходные данные, результаты, а также инструкции, нормативы, стандарты, критерии и т. п.

Современная технология программирования — компонентный подход , который предполагает построение программного обеспечения из отдельных компонентов — физически отдельно существующих частей программного обеспечения, которые взаимодействуют между собой через стандартизованные двоичные интерфейсы. В настоящее время критериями качества программного продукта принято считать:− функциональность ; − надежность ;− легкость применения ;− эффективность (отношение уровня услуг, предоставл-х программным продуктом пользов-лю при заданных условиях, к объему используемых ресурсов);− сопровождаемость (характер-ки программ-го продукта, которые позволяют минимизир-ть усилия по внесению изменений для устранения в нем ошибок и по его модификации в соотв-вии с изменяющ-ся потребностями пользов-лей);− мобильность (способность ПС быть перенесенным из одной среды в другую, в частности, с одной ЭВМ на др.).

Важным этапом создания прогр-го продукта явл. тестирование и отладка.

Отладка − это деятельность, направленная на обнаружение и исправление ошибок в программном продукте с использованием процессов выполнения его программ.

Тестирование − это процесс выполнения его программ на некотором наборе данных, для которого заранее известен результат применения или известны правила поведения этих программ.

Существуют следующие методы тестирования ПС:

1) Статическое тестирование – ручная проверка программы за столом.

2) Детерминированное тестир-е – при разл-х комбинациях исх-х данных.

3) Стохастическое – исх. данные выбир-ся произвольно, на выходе определяется качеств-е совпадение результатов или примерная оценка.

Стиль программирования — набор приемов или методов программирования, которые используют программисты, чтобы получить правильные, эффективные, удобные для применения и легкочитаемые программы.

Существует несколько стилей программирования:

  1. Процедурное программирование – это программирование, при котором программа представляет собой последовательность операторов. Используется в языках высокого уровня Basic, Fortran и др.
  2. Функциональное программирование – это программирование, при котором программа представляет собой последовательность вызовов функций. Используется в языках Lisp и др.
  3. Логическоепрограммирование – это программирование, при котором программа представляет собой совокупность определения соотношений между объектами. Используется в языках Prolog и др.

Объектно-ориентированноепрограммирование – это программирование, при котором основой программы является объект представляющий собой совокупность данных и правил их преобразования. Используется в языках Turbo-Pascal, C++ и др.

Программы на традиционных языках программирования, таких как Си, Паскаль, Java и т.п. состоят их последовательности модификаций значений некоторого набора переменных, который называется состоянием . Если не рассматривать операции ввода-вывода, а также не учитывать того факта, что программа может работать непрерывно (т.е. без остановок, как в случае серверных программ), можно сделать следующую абстракцию. До начала выполнения программы состояние имеет некоторое начальное значение σ0 , в котором представлены входные значения программы. После завершения программы состояние имеет новое значение σ0 , включающее в себя то, что можно рассматривать как «результат» работы программы. Во время исполнения каждая команда изменяет состояние; следовательно, состояние проходит через некоторую конечную последовательность значений:

σ = σ0 → σ1 → σ2 → · · · → σn = σ0

Состояние модифицируется с помощью команд присваивания , записываемых в виде v=E или v:=E, где v — переменная, а E — некоторое выражение. Эти команды следуют одна за другой; операторы, такие как if и while, позволяют изменить порядок выполнения этих команд в зависимости от текущего значения состояния. Такой стиль программирования называютимперативным илипроцедурным .

Функциональное программирование представляет парадигму, в корне отличную от представленной выше модели. Функциональная программа представляет собой некоторое выражение (в математическом смысле); выполнение программы означает вычисление значения этого выражения.1 С учетом приведенных выше обозначений, считая что результат работы

1 Употребление термина «вычисление» не означает, что программа может оперировать только с числами; результатом вычисления могут оказаться строки, списки и вообще, любые допустимые в языке структуры данных.

императивной программы полностью и однозначно определен ее входом, можно сказать, что финальное состояние (или любое промежуточное) представляет собой некоторую функцию (в математическом смысле) от начального состояния, т.е. σ0 = f(σ). В функционально программировании используется именно эта точка зрения: программа представляет собой выражение, соответствующее функции f. Функциональные языки программирования поддерживают построение таких выражений, предоставляю широкий выбор соответствующих языковых конструкций.

При сравнении функционального и императивного подхода к программированию можно заметить следующие свойства функциональных программ:

Функциональные программы не используют переменные в том смысле, в котором это слово употребляется в императивном программировании. В частности в функциональных программах не используется оператор присваивания.

Как следствие из предыдущего пункта, в функциональных программах нет циклов.

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

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

Вместо циклов функциональные программы широко используют рекурсивные функции.

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

Прежде всего, императивный стиль в программировании не является жестко заданной необходимостью. Многие характеристики императивных языков программирования являются результатом абстрагирования от низкоуровневых деталей реализации компьютера, от машинных кодов к языкам ассемблера, а затем к языкам типа Фортрана и т.д. Однако нет причин полагать, что такие языки отражают наиболее естественный для

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

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

Например, рассмотрим следующую программу на языке Haskell:

factorial n = if n == 0 then 1 else n * factorial (n — 1)

Практически сразу видно, что эта программа соответствует следующей частичной функции:

(Здесь символ означает неопределенность функции, поскольку при отрицательных значениях аргумента программа не завершается.) Однако для программы на языке Си это соответствие не очевидно:

int x = 1; while (n > 0)

x = x * n; n = n — 1;

Следует также сделать замечание относительно употребления термина «функция» в таких языках как Си, Java и т.п. В математическом смысле «функции» языка Си не являются функциями, поскольку:

Их значение может зависеть не только от аргументов;

Результатом их выполнения могут быть разнообразные побочные эффекты (например, изменение значений глобальных переменных)

Два вызова одной и той же функции с одними и теми же аргументами могут привести к различным результатам.

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

2 Основы лямбда-исчисления

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

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

В настоящее время лямбда-исчисление является основной из таких формализаций, применяемой в исследованиях связанных с языками программирования. Связано это, вероятно, со следующими факторами:

Это единственная формализация, которая, хотя и с некоторыми неудобствами, действительно может быть непосредственно использована для написания программ.

Лямбда-исчисление дает простую и естественную модель для таких важных понятий, как рекурсия и вложенные среды.

Большинство конструкций традиционных языков программирования может быть более или менее непосредственно отображено в конструкции лямбда-исчисления.

Функциональные языки являются в основном удобной формой синтаксической записи для конструкций различных вариантов лямбдаисчисления. Некоторые современные языки (Haskell, Clean) имеют

100% соответствие своей семантики с семантикой подразумеваемых конструкций лямбда-исчисления.

В математике, когда необходимо говорить о какой-либо функции, принято давать этой функции некоторое имя и впоследствии использовать его, как, например, в следующем утверждении:

Пусть f: R → R определяется следующим выражением:

Тогда f0 (x) не интегрируема на интервале .

Многие языки программирования также допускают определение функций только с присваиванием им некоторых имен. Например, в языке Си функция всегда должна иметь имя. Это кажется естественным, однако поскольку в функциональном программировании функции используются повсеместно, такой подход может привести к серьезным затруднениям. Представьте себе, что мы должны всегда оперировать с арифметическими выражениями в подобном стиле:

Пусть x = 2 и y = 4. Тогда xx = y.

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

где E — некоторое выражение, возможно, использующее переменную x.

Пример. λx.x2 представляет собой функцию, возводящую свой аргумент в квадрат.

Использование лямбда-нотации позволяет четко разделить случаи, когда под выражением вида f(x) мы понимаем саму функцию f и ее значение в точке x. Кроме того, лямбда-нотация позволяет формализовать практически все виды математической нотации. Если начать с констант и переменных и строить выражения только с помощью лямбда-выраже- ний и применений функции к аргументам, то можно представить очень сложные математические выражения.

Применение функции f к аргументу x мы будем обозначать как f x, т.е., в отличие от того, как это принято в математике, не будем использовать скобки2 . По причинам, которые станут ясны позднее, будем считать, что применение функции к аргументу ассоциативно влево, т.е. f x y

2 Заметим, что и в математике такие выражения, как sin x записываются без скобок.

означает (f(x))(y). В качестве сокращения для выражений вида λx.λy.E будем использовать запись λx y.E (аналогично для большего числа аргументов). Также будем считать, что «область действия» лямбда-выра- жения простирается вправо насколько возможно, т.е., например, λx.x y означает λx.(x y), а не (λx.x)y.

На первый взгляд кажется, что нам необходимо ввести специальное обозначение для функций нескольких аргументов. Однако существует операция каррирования 3 , позволяющая записать такие функции в обычной лямбда-нотации. Идея заключается в том, чтобы использовать выражения вида λx y.x + y. Такое выражение можно рассматривать как функцию R → (R → R), т.е. если его применить к одному аргументу, результатом будет функция, которая затем принимает другой аргумент. Таким образом:

(λx y.x + y) 1 2 = (λy.1 + y) 2 = 1 + 2.

Переменные в лямбда-выражениях могут бытьсвободными исвязанными . В выражении вида x2 + x переменная x является свободной; его значение зависит от значения переменной x и в общем случае ее нельзя

вать обозначение j, значение выражения не изменится.

Следует понимать, что в каком-либо подвыражении переменная может быть свободной (как в выражении под интегралом), однако во всем выражении она связана какой-либо операцией связывания переменной , такой как операция суммирования. Та часть выражения, которая находится «внутри» операции связывания, называется областью видимости переменной.

В лямбда исчислении выражения λx.E[x] и λy.E[y] считаются эквивалентными (это называется α-эквивалентностью, и процесс преобразования между такими парами называют α-преобразованием). Разумеется, необходимо наложить условие, что y не является свободной переменной в E[x].

3 от фамилии известного логика Хаскелла Карри, в честь которого назван язык программирования Haskell

3 Лямбда-исчисление как формальная система

Лямбда-исчисление основано на формальной нотации лямбда-терма, составляемого из переменных и некоторого фиксированного набора констант с использованием операции применения функции и лямбда-абстра- гирования. Сказанное означает, что все лямбда-выражения можно разделить на четыре категории:

1. Переменные: обозначаются произвольными строками, составленными из букв и цифр.

2. Константы: также обозначаются строками; отличие от переменных будем определять из контекста.

3. Комбинации: , т.е. применения функции S к аргументу T ; и S и T могут быть произвольными лямбда-термами. Комбинация записывается как S T .

4. Абстракции произвольного лямбда-терма S по переменной x, обозначаемые как λx.S.

Таким образом, лямбда-терм определяется рекурсивно и его грамматику можно определить в виде следующей формы Бэкуса-Наура:

Exp = Var| Const| Exp Exp| λ Var . Exp

В соответствие с этой грамматикой лямбда-термы представляются в виде синтаксических деревьев, а не в виде последовательности символов. Отсюда следует, что соглашения об ассоциативности операции применения функции, эквивалентность выражений вида λx y.S и λx.λy.S, неоднозначность в именах констант и переменных проистекают только из необходимости представления лямбда-термов в удобном человеку виде, и не являются частью формальной системы.

3.1 Свободные и связанные переменные

В данном разделе мы формализуем данное ранее интуитивное представление о свободных и связанных переменных. Множество свободных

переменных F V (S) лямбда-терма S можно определить рекурсивно следующим образом:

Аналогично множество связанных переменных BV (S) определяется следующими формулами:

BV (S T) = BV (S) BV (T)

Здесь предполагается, что c — некоторая константа.

Пример. Для терма S = (λx y.x) (λx.z x) можно показать, что F V (S) = и

Интуитивно ясно, что применение терма λx.S как функции к аргументу T дает в результате терм S, в котором все свободные вхождения переменной x заменены на T . Как ни странно, формализовать это интуитивное представление оказывается нелегко.

Будем обозначать операцию подстановки терма S вместо переменной x в другом терме T как T . Также, как и в определение свободных и связанных переменных, правила подстановки также можно определить рекурсивно. Трудность состоит в том, что необходимо наложить дополнительные ограничения, позволяющие избегать конфликта в именах переменных.

Лямбда-исчисление основано на трех операциях конверсии, которые позволяют переходить от одного терма к другому, эквивалентному ему. По сложившейся традиции эти конверсии обозначают греческими буквами α, β и η. Они определяются следующим образом:

α-конверсия: λx.S −→ λy.S при условии, что y / F V (S).

Например, λu.u v −→ λw.w u.

β-конверсия: (λx.S) T −→ S.

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

3.4 Равенство лямбда-термов

Используя введенные правила конверсии, можно формально определить понятие равенства лямбда-термов. Два терма равны, если от одного из них можно перейти к другому с помощью конечной последовательности конверсий. Определим понятие равенства следующими выражениями, в которых горизонтальные линии следует понимать как «если утверждение над чертой выполняется, то выполняется и утверждение

Следует отличать понятие равенства, определяемое этими формулами, от понятия синтаксической эквивалентности, которую мы будем обозначать специальным символом ≡. Например, λx.x 6≡λy.y, но λx.x = λy.y. Часто можно рассматривать синтаксическую эквивалентность термов с точностью до α-конверсий. Такую эквивалентность будем обозначать символом ≡α . Это отношение определяется так же, как равенство лямбда-термов, за тем исключением, что из всех конверсий допустимы только α-конверсии. Таким образом, λx.x ≡α λy.y.

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

— ООП не сможет больше спасать нас от «Облачных монстров».

Примечание переводчика: Есть два понятия — параллельность (выполнение одновременно, независимо) и конкурентность (выполнение по шагам, поочерёдно, но одновременно несколько задач) и как всегда, мне пришлось поломать голову подобрая правильные термины.

Некоторые слова или термины я буду дублировать в скобках в оригинале, для того, чтобы искать по англоязычным терминам дополнительную информацию, которой будет в разы больше.

Возможно вы уже слышали такое выражение, вроде: “Clojure”, “Scala”, “Erlang” или даже “Java теперь имеет лямбды”. И вы имеете хоть и отдалённое представление о «Функциональном программировании». Если вы участник какого-либа программисткого сообщества, тогда эта тема могла уже вами обсуждаться.

Если вы поищите в Google по словосочетанию «Функциональное программирование», вы не увидите что-то нового. Второй язык из созданных ранее уже охватывает эту тему, он был создан в 50-ых и называется Lisp. Тогда, какого чёрта, эта тема стала популярна только сейчас? Всего то 60 лет спустя?

В начале, компьютеры были очень медленными

Верите вы этому или нет, но компьютеры были нааамного медленнее чем DOM. Нет, действительно. И в то-же время были 2 основные идеи в соглашении по дизайну и реализации языков программирования:

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

Функции являются абстракциями , в которых детали реализации некоторого действия скрываются за отдельным именем. Хорошо написанный набор функций позволяет использовать их много раз. Стандартная библиотека Python содержит множество готовых и отлаженных функций, многие из которых достаточно универсальны, чтобы работать с широким спектром входных данных. Даже если некоторый участок кода не используется несколько раз, но по входным и выходным данным он достаточно автономен, его смело можно выделить в отдельную функцию.

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

Далее будут подробно рассмотрены описание и использование функций в Python , рекурсия , передача и возврат функций в качестве параметров, обработка последовательностей и итераторы , а также такое понятие как генератор . Будет продемонстрировано, что в Python функции являются объектами (и, значит, могут быть переданы в качестве параметров и возвращены в результате выполнения функций). Кроме того, речь пойдет о том, как можно реализовать некоторые механизмы функционального программирования, не имеющие в Python прямой синтаксической поддержки, но широко распространенные в языках функционального программирования.

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

Функциональное программирование — это стиль программирования, использующий только композиции функций . Другими словами, это программирование в выражениях, а не в императивных командах.

Как отмечает Дэвид Мертц (David Mertz) в своей статье о функциональном программировании на Python , «функциональное программирование — программирование на функциональных языках ( LISP , ML, OCAML, Haskell, . )», основными атрибутами которых являются:

  • «Наличие функций первого класса» (функции наравне с другими объектами можно передавать внутрь функций).
  • Рекурсия является основной управляющей структурой в программе.
  • Обработка списков (последовательностей).
  • Запрещение побочных эффектов у функций, что в первую очередь означает отсутствие присваивания (в «чистых» функциональных языках)
  • Запрещение операторов, основной упор делается на выражения. Вместо операторов вся программа в идеале — одно выражение с сопутствующими определениями.
  • Ключевой вопрос: что нужно вычислить, а не как .
  • Использование функций более высоких порядков (функции над функциями над функциями).

Функциональная программа

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

Программы в функциональном стиле конструируются как композиция функций. При этом функции понимаются почти так же, как и в математике: они отображают одни объекты в другие. В программировании «чистые» функции — идеал, не всегда достижимый на практике. Практически полезные функции обычно имеют побочный эффект : сохраняют состояние между вызовами или меняют состояние других объектов. Например, без побочных эффектов невозможно представить себе функции ввода-вывода. Собственно, такие функции ради этих «эффектов» и используются. Кроме того, математические функции легко работают с объектами, требующими бесконечного объема информации (например, вещественные числа). В общем случае компьютерная

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

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным.

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

Чистые функции

Чистая функция максимально проста. Она должна всегда возвращать один и тот же результат. Посмотрите на эту JavaScript-функцию:

var z = 10; function add(x, y)

function add (x , y ) <

Конечно, это искусственный пример, но он очень хорошо демонстрирует, как должна работать чистая функция. При одних и тех же значениях x и y мы всегда будем получать одинаковый результат функции. Предсказуемость – важная часть работы программы в функциональном программировании.

Изменяемые данные и побочные эффекты

Вернемся к примеру кода. Если мы добавим в качестве аргумента функции add() , переменную z , которая объявлена выше, наша функция перестанет быть чистой и предсказуемой. Почему? Потому что z объявлена как обычная переменная: она доступна для изменения из любого места программы.

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

Корректный код чистой функции с z должен выглядеть так:

const x = 10; const z = 10; add (x, z); // вернет 20

add (x , z ) ; // вернет 20

В этом случае функция всегда будет возвращать предсказуемый результат. Если функция не будет работать предсказуемо – это приведет к нежелательным побочным эффектам.

Еще один пример не функционального кода – классические циклы. Вспомним, как выглядит типичный цикл for в JavaScript:

var acc = 0; for (var i = 1; i end) < return acc; >else < return sumRange(start + 1, end, acc + start); >> console.log(sumRange(1, 10, 0)); // выведет 55

function sumRange (start , end , acc ) <

return sumRange (start + 1 , end , acc + start ) ;

console . log (sumRange (1 , 10 , 0 ) ) ; // выведет 55

Такая конструкция позволяет использовать константы для определения начала, конца цикла и шага. В основе такого типа цикла лежит идея вызова функции внутри себя, или рекурсивного вызова. В примере выше функция sumRange() с заданными аргументами делает проверку условия, и в случае ложного результата вызывает саму себя с измененными аргументами.

Композиция функций

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

function addOne(x) < return x + 1; >function timesTwo(x) < return x * 2; >console.log(addOne(timesTwo(3))); // выведет 7 console.log(timesTwo(addOne(3))); // выведет 8

function addOne (x ) <

function timesTwo (x ) <

В примере выше мы описали две простые функции: addOne (прибавляет к аргументу единицу) и timesTwo (умножает аргумент на два). Техника компоновки позволяет нам вызывать две эти функции друг в друге в разном порядке. В результате, с разным логическим порядком вызова чистых функций и одинаковым значением аргумента мы получили более сложный функционал, который дает нам необходимый результат и делает это предсказуемо.

Польза функционального программирования

Функциональное программирование помогает сделать код более чистым, предсказуемым и легко читаемым. Использование принципов ФП помогает избавиться от лишних абстракций с непредсказуемым поведением, следовательно, сделать работу программы более предсказуемой и снизить количество возможных ошибок.

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

Функциональное программирование плохо подходит для алгоритмов, основанных на графах из-за сравнительно более медленной работы программы. ФП в целом плохо применимо в решениях, которые годами основывались на использовании императивного подхода.

Самое важное – в функциональных языках нет эффективного неупорядоченного словаря множества. В сравнении с хэш-таблицами, функциональные словари работают значительно хуже, а это может быть критично для некоторых приложений. Однако указанные недостатки можно отнести только к императивным языкам.

Функциональное программирование в языках

Так как функциональное программирование – это прежде всего подход к написанию кода, использовать его принципы можно в любом языке. Однако существуют языки, специально заточенные под функциональный подход. Первый и самый известный из них – Lisp. Он появился еще в 1958 году. Его автор – Джон Маккарти, информатик и автор термина «искусственный интеллект». Lisp по сей день популярен в среде проектировщиков ИИ.

Более современные функциональные языки, такие как Elm и Elixir, по данным GitHub и Stack Overflow постепенно и уверенно набирают популярность. Рост популярности JavaScript также привел к повышенному интересу к концепциям функционального программирования для применения в этом языке.

Функции являются абстракциями , в которых детали реализации некоторого действия скрываются за отдельным именем. Хорошо написанный набор функций позволяет использовать их много раз. Стандартная библиотека Python содержит множество готовых и отлаженных функций, многие из которых достаточно универсальны, чтобы работать с широким спектром входных данных. Даже если некоторый участок кода не используется несколько раз, но по входным и выходным данным он достаточно автономен, его смело можно выделить в отдельную функцию.

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

Далее будут подробно рассмотрены описание и использование функций в Python , рекурсия , передача и возврат функций в качестве параметров, обработка последовательностей и итераторы , а также такое понятие как генератор . Будет продемонстрировано, что в Python функции являются объектами (и, значит, могут быть переданы в качестве параметров и возвращены в результате выполнения функций). Кроме того, речь пойдет о том, как можно реализовать некоторые механизмы функционального программирования, не имеющие в Python прямой синтаксической поддержки, но широко распространенные в языках функционального программирования.

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

Функциональное программирование — это стиль программирования, использующий только композиции функций . Другими словами, это программирование в выражениях, а не в императивных командах.

Как отмечает Дэвид Мертц (David Mertz) в своей статье о функциональном программировании на Python , «функциональное программирование — программирование на функциональных языках ( LISP , ML, OCAML, Haskell, . )», основными атрибутами которых являются:

  • «Наличие функций первого класса» (функции наравне с другими объектами можно передавать внутрь функций).
  • Рекурсия является основной управляющей структурой в программе.
  • Обработка списков (последовательностей).
  • Запрещение побочных эффектов у функций, что в первую очередь означает отсутствие присваивания (в «чистых» функциональных языках)
  • Запрещение операторов, основной упор делается на выражения. Вместо операторов вся программа в идеале — одно выражение с сопутствующими определениями.
  • Ключевой вопрос: что нужно вычислить, а не как .
  • Использование функций более высоких порядков (функции над функциями над функциями).

Функциональная программа

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

Программы в функциональном стиле конструируются как композиция функций. При этом функции понимаются почти так же, как и в математике: они отображают одни объекты в другие. В программировании «чистые» функции — идеал, не всегда достижимый на практике. Практически полезные функции обычно имеют побочный эффект : сохраняют состояние между вызовами или меняют состояние других объектов. Например, без побочных эффектов невозможно представить себе функции ввода-вывода. Собственно, такие функции ради этих «эффектов» и используются. Кроме того, математические функции легко работают с объектами, требующими бесконечного объема информации (например, вещественные числа). В общем случае компьютерная

String reverse(String arg) < if(arg.length == 0) < return arg; >else < return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); >>
Эта функция довольно медленная, потому что она повторно вызывает сама себя . Здесь возможна утечка памяти, так как множество раз создаются временные объекты. Но это функциональный стиль. Вам может показать странным, как люди могут так программировать. Ну, я как раз собирался вам рассказать.

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

Unit тестирование

Вот она, голубая мечта unit-тестеров. Можно протестировать каждую функцию в программе используя только нужные аргументы. Нет необходимости вызывать функции в правильном порядке или воссоздавать правильное внешнее состояние. Всё что вам нужно, это передать аргументы, которые соответствуют граничным случаям. Если все функции в вашей программе проходят Unit-тесты, то вы можете быть намного более уверены в качестве вашего ПО, чем в случае императивных языков программирования. В Java или C++ проверки возвращаемого значения не достаточно — функция может поменять внешнее состояние, которое тоже подлежит проверке. В ФП такой проблемы нет.

Отладка

Как только вы воспроизведёте ошибку, найти её источник — тривиальная задача. Это даже приятно. Как только вы остановите выполнение программы, перед вами будет весь стек вызовов. Вы можете просмотреть аргументы вызова каждой функции, прямо как в императивном языке. С тем отличием, что в императивной программе этого не достаточно, ведь функции зависят от значений полей, глобальных переменных и состояний других классов. Функция в ФП зависит только от своих аргументов, и эта информация оказывается прямо у вас перед глазами! Даже больше, в императивной программе проверки возвращаемого значения не достаточно для того, чтобы сказать, правильно ли ведёт себя кусок кода. Вам придётся выследить десятки объектов за пределами функции, чтобы удостовериться, что всё работает правильно. В функциональном программировании всё, что нужно сделать — это взглянуть на возвращаемое значение!

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

Многопоточность

Если дела обстоят подобным образом, то почему так редко функциональные языки программирования используются в многопоточных приложениях? На самом деле чаще, чем вы думаете. Компания Ericsson разработала функциональный язык под названием Erlang для использования на отказоустойчивых и масштабируемых телекоммуникационных коммутаторах. Многие отметили преимущества Erlang-а и стали его использовать . Мы говорим о телекоммуникациях и системах контроля трафика, которые далеко не так просто масштабируются, как типичные системы, разработанные на Wall Street. Вообще-то, системы написанные на Erlang, не такие масштабируемые и надёжные, как Java системы. Erlang системы просто сверхнадёжные.

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

Компилятор функционального языка может проанализировать код, классифицировать функции, которые создают строки s1 и s2 , как функции потребляющие много времени, и запустить их параллельно. Это невозможно сделать в императивном языке, потому что каждая функция может изменять внешнее состояние и код, идущий непосредственно после вызова, может зависеть от неё. В ФП автоматический анализ функций и поиск подходящих кандидатов для распараллеливания — это тривиальнейшая задача, как автоматический inline ! В этом смысле функциональный стиль программирования соответствует требованиям завтрашнего дня. Разработчики железа уже не могут заставить CPU работать быстрее. Вместо этого они наращивают количество ядер и заявляют о четырёхкратном увеличении скорости многопоточных вычислений. Конечно они очень вовремя забывают сказать, что ваш новый процессор покажет прирост только в программах, разработанных с учётом распараллеливания. Среди императивного ПО таких очень мало. Зато 100% функциональных программ готовы к многопоточности из коробки.

Развёртывание по горячему

В идеале нужно обновить все нужные участки кода не останавливая систему в принципе. В императивном мире это невозможно [пер. в Smalltalk-е очень даже возможно]. Представьте себе выгрузку Java класса на лету и перезагрузка новой версии. Если бы мы так сделали, то все экземпляры класса стали бы нерабочими, потому что потерялось бы состояние, которое они хранили. Нам пришлось бы писать хитрый код, для контроля версий. Пришлось бы серриализовать все созданные экземпляры класса, потом уничтожить их, создать экземпляры нового класса, попытаться загрузить серриализованные данные в надежде, что миграция пройдёт нормально и новые экземпляры будут валидными. И кроме того, миграционный код необходимо писать каждый раз вручную. И ещё миграционный код должен сохранять ссылки между объектами. В теории ещё куда ни шло, но на практике это никогда не заработает.

В функциональной программе всё состояние хранится в стеке в виде аргументов функций. Это позволяет значительно упростить развёртывание по горячему! По сути всё что нужно сделать — это вычислить разницу между кодом на рабочем сервере и новой версией, и установить изменения в коде. Остальное будет сделано языковыми инструментами автоматически! Если вы думаете, что это научная фантастика, то дважды подумайте. Инженеры, имеющие дело с Erlang, годами обновляют свои системы без остановки их работы.

Доказательные вычисления и оптимизация (Machine Assisted Proofs and Optimizations)

Дополнительно вы можете использовать математический аппарат, чтобы доказать корректность участков ваших программ. При желании можно написать инструменты, которые анализируют код и автоматически создают Unit-тесты для граничных случаев! Такая функциональность бесценна для сверхнадёжных систем (rock solid systems). При разработке систем контроля кардиостимуляторов или управления воздушным трафиком такие инструменты просто необходимы. Если же ваши разработки не находятся в сфере критически важных приложений, то инструменты автоматической проверки всё равно дадут вам гигантское преимущество перед вашими конкурентами.

Функции высшего порядка

В ФП функция — это не тоже самое, что функция в Java или C. Это надмножество — они могут тоже самое, что Java функции и даже больше. Пусть у нас есть функция на C:

Int add(int i, int j) < return i + j; >
В ФП это не тоже самое, что обычная C функция. Давайте расширим наш Java компилятор, чтобы он поддерживал такую запись. Компилятор должен превратить объявление функции в следующий Java код (не забывайте, что везде присутствует неявный final):

> Символ add не совсем функция. Это маленький класс с одним методом. Теперь мы можем передавать add в качестве аргумента в другие функции. Мы можем записать его в другой символ. Мы можем создавать экземпляры add_function_t в runtime и они будут уничтожены сборщиком мусора, если станут ненужными. Функции становятся базовыми объектами, как числа и строки. Функции, которые оперируют функциями (принимают их в качестве аргументов) называются функциями высшего порядка. Пусть это вас не пугает. Понятие функций высшего порядка почти не отличается от понятия Java классов, которые оперируют друг другом (мы можем передавать классы в другие классы). Мы можем называть их «классы высшего порядка», но никто этим не заморачивается, потому что за Java не стоит строгое академическое сообщество.

Как и когда нужно использовать функции высшего порядка? Я рад, что вы спросили. Вы пишите свою программу как один большой монолитный кусок кода не заботясь об иерархии классов. Если вы увидите, что какой-то участок кода повторяется в разных места, вы выносите его в отдельную функцию (к счастью в школах еще учат как это делать). Если вы замечаете, что часть логики в вашей функции должна вести себя по разному в некоторых ситуациях, то вы создаёте функцию высшего порядка. Запутались? Вот реальный пример из мой работы.

Предположим, что у нас есть участок Java кода, который получает сообщение, преобразует его различными способами и передаёт на другой сервер.

Void handleMessage(Message msg) < // . msg.setClientCode("ABCD_123"); // . sendMessage(msg); >// . >
Теперь представьте себе, что система поменялась, и теперь нужно распределять сообщения между двумя серверами вместо одного. Всё остаётся неизменным, кроме кода клиента — второй сервер хочет получать этот код в другом формате. Как нам справиться с этой ситуацией? Мы можем проверять, куда должно попасть сообщение, и в зависимости от этого устанавливать правильный код клиента. Например так:

Class MessageHandler < void handleMessage(Message msg) < // . if(msg.getDestination().equals("server1") < msg.setClientCode("ABCD_123"); >else < msg.setClientCode("123_ABC"); >// . sendMessage(msg); > // . >
Но такой подход плохо масштабируется. При добавлении новых серверов функция будет расти линейно, и внесение изменений превратится в кошмар. Объектно ориентированный подход заключается в выделении общего суперкласса MessageHandler и вынесение логики определения кода клиента в подклассы:

Abstract class MessageHandler < void handleMessage(Message msg) < // . msg.setClientCode(getClientCode()); // . sendMessage(msg); >abstract String getClientCode(); // . > class MessageHandlerOne extends MessageHandler < String getClientCode() < return "ABCD_123"; >> class MessageHandlerTwo extends MessageHandler < String getClientCode() < return "123_ABCD"; >>
Теперь для каждого сервера мы можем создать экземпляр соответствующего класса. Добавление новых сервером становится более удобным. Но для такого небольшого изменения многовато текста. Пришлось создать два новых типа чтобы просто добавить поддержку различного кода клиента! Теперь сделаем тоже самое в нашем языке с поддержкой функций высшего порядка:

; > MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
Мы не создавали новых типов и не усложняли иерархию классов. Мы просто передали функцию в качестве параметра. Мы достигли того же эффекта, как и в объектно-ориентированном аналоге, только с некоторыми преимуществами. Мы не привязывали себя к какой-либо иерархии классов: мы можем передавать любые другие функции в runtime и менять их в любой момент, сохраняя при этом высокий уровень модульности меньшим количеством кода. По сути компилятор создал объектно-ориентированный «клей» вместо нас! При этом сохраняются все остальные преимущества ФП. Конечно абстракции, предлагаемые функциональными языками на этом не заканчиваются. Функции высшего порядка это только начало

Каррирование

Функциональные языки невероятно выразительны. В функциональном языке вам не понадобятся паттерны проектирования, потому что язык настолько высокоуровневый, что вы легко начнёте программировать в концепциях, которые исключают все известные паттерны программирования. Одним из таких паттернов является Адаптер (чем он отличается от Фасада? Похоже, что кому-то понадобилось наштамповать побольше страниц, чтобы выполнить условия контракта). Этот паттерн оказывается ненужным если в языке есть поддержка каррирования.

Паттерн Адаптер наиболее часто применяется к «стандартной» единице абстракции в Java — классу. В функциональных языках паттерн применяется к функциям. Паттерн берёт интерфейс и преобразует его в другой интерфейс, согласно определённым требованиям. Вот пример паттерна Адаптер:

Int pow(int i, int j); int square(int i) < return pow(i, 2); >
Этот код адаптирует интерфейс функции, возводящей число в произвольную степень, к интерфейсу функции, которая возводит число в квадрат. В аккадемических кругах этот простейший приём называется каррирование (в честь специалиста по логике Хаскелла Карри (Haskell Curry), который провёл ряд математических трюков, чтобы всё это формализовать). Так как в ФП функции используются повсеместно в качестве аргументов, каррирование используется очень часто, чтобы привести функции к интерфейсу, необходимому в том или ином месте. Так как интерфейс функции — это её аргументы, то каррирование используется для уменьшения количества аргументов (как в примере выше).

Этот инструмент является встроенным в функциональные языки. Вам не нужно вручную создавать функцию, которая оборачивает оригинал. Функциональный язык сделает всё за вас. Как обычно давайте расширим наш язык, добавив в него каррирование.

Square = int pow(int i, 2);
Этой строкой мы автоматически создаём функцию возведения в квадрат с одним аргументом. Новая функция будет вызывать функцию pow , подставляя 2 в качестве второго аргумента. С точки зрения Java, это будет выглядеть следующим образом:

> Как видите, мы просто написали обёртку над оригинальной функцией. В ФП каррирование как раз и представляет из себя простой и удобный способ создания обёрток. Вы сосредотачиваетесь на задаче, а компилятор пишет необходимый код за вас! Всё очень просто, и происходит каждый раз, когда вы хотите использовать паттерн Адаптер (обёртку).

Ленивые вычисления

String s1 = somewhatLongOperation1(); String s2 = somewhatLongOperation2(); String s3 = concatenate(s1, s2);
В императивных языках программирования очерёдность вычисления не вызывает никаких вопросов. Поскольку каждая функция может повлиять или зависеть от внешнего состояния, то необходимо соблюдать чёткую очерёдность вызовов: сначала somewhatLongOperation1 , затем somewhatLongOperation2 , и concatenate в конце. Но не всё так просто в функциональных языках.

Как мы уже видели ранее somewhatLongOperation1 и somewhatLongOperation2 могут быть запущены одновременно, потому что функции гарантированно не влияют и не зависят от глобального состояния. Но что, если мы не хотим выполнять их одновременно, нужно ли вызывать их последовательно? Ответ — нет. Эти вычисления должны быть запущены, только если какая-либо другая функция зависит от s1 и s2 . Нам даже не нужно выполнять их до тех пор, пока они понадобятся внутри concatenate . Если вместо concatenate мы подставим функцию, которая в зависимости от условия использует один аргумент из двух, то второй аргумент можно даже не вычислять! Haskell — это пример языка с отложенными вычислениями. В Haskell отсутствует гарантия какой-либо очередности вызовов (вообще!), потому что Haskell выполняет код по мере необходимости.

Ленивые вычисления обладают рядом достоинств как и некоторыми недостатками. В следующем разделе мы обсудим достоинства и я объясню как уживаться с недостатками.

Оптимизация

Абстрагирование структур управления

Unless(stock.isEuropean()) < sendToSEC(stock); >
Мы хотим, чтобы функция sendToSEC выполнялась только если фонд (stock) не европейский. Как можно реализовать unless ? Без ленивый вычислений нам бы понадобилась система макросов, но в языках, подобных Haskell, это не обязательно. Мы можем объявить unless в виде функции!

Void unless(boolean condition, List code) < if(!condition) code; >
Заметьте, что code не будет выполняться, если condition == true . В строгих языках такое поведение невозможно повторить, так как аргументы будут вычислены прежде, чем unless будет вызвана.

Бесконечные структуры данных

Недостатки

В ленивом языке никто не гарантирует, что первая строка выполнится раньше второй! Это означает, что мы не можем делать ввод-вывод, не можем нормально использовать нативные функции (ведь их нужно вызывать в определённом порядке, чтобы учитывать их побочные эффекты), и не можем взаимодействовать с внешним миром! Если мы введём механизм для упорядочивания выполнения кода, то потеряем преимущество математической строгости кода (а следом потеряем все плюшки функционального программирования). К счастью ещё не всё потеряно. Математики взялись за работу и придумали несколько приёмов для того, чтобы убедится в правильном порядке выполняемых инструкций не потеряв функционального духа. Мы получили лучшее от двух миров! Такие приёмы включают в себя продолжения (continuation), монады (monads) и однозначная типизация (uniqueness typing). В данной статье мы поработаем с продолжениями, а монады и однозначную типизацию отложим до следующего раза. Занятно, что продолжения очень полезная штука, которая используется не только для задания строгого порядка вычислений. Об этом мы тоже поговорим.

Продолжения

Когда мы рассматривали функции, мы изучили лишь половину правды, ведь мы исходили из предположения, что функция возвращает значение в вызывающую её функцию. В этом смысле продолжение — это обобщение функций. Функция не обязательно должна возвращать управление в то место, откуда её вызвали, а может возвращать в любое место программы. «Продолжение» — это параметр, который мы можем передать в функцию, чтобы указать точку возврата. Звучит намного страшнее, чем есть на самом деле. Давайте взглянем на следующий код:

Int i = add(5, 10); int j = square(i);
Функция add возвращает число 15, которое записывается в i , в том месте, где функция и была вызвана. Затем значение i используется при вызове square . Заметьте, что ленивый компилятор не может поменять очередность вычислений, ведь вторая строка зависит от результата первой. Мы можем переписать этот код с использованием Стиль Передачи Продолжения (Continuation Passing Style или CPS), когда add возвращает значение в функцию square .


Int j = add(5, 10, square);
В таком случае add получает дополнительный аргумент — функцию, которая будет вызвана после того, как add закончит работать. В обоих примерах j будет равен 225.

В этом и заключается первый приём, позволяющий задать порядок выполнения двух выражений. Вернёмся к нашему примеру с вводом-выводом

System.out.println(«Please enter your name: «); System.in.readLine();
Эти две строки не зависят друг от друга, и компилятор волен поменять их порядок по своему хотению. Но если мы перепишем в CPS, то тем самым добавим нужную зависимость, и компилятору придётся проводить вычисления одно за другим!

System.out.println(«Please enter your name: «, System.in.readLine);
В таком случае println должен будет вызвать readLine , передав ему свой результат, и вернуть результат readLine в конце. В таком виде мы можем быть уверены, что эти функции будут вызваны по очереди, и что readLine вообще вызовется (ведь компилятор ожидает получить результат последней операции). В случае Java println возвращает void . Но если бы возвращалось какое-либо абстрактное значение (которое может служить аргументом readLine), то это решило бы нашу проблему! Конечно выстраивание таких цепочек функций сильно ухудшает читаемость кода, но с этим можно бороться. Мы можем добавить в наш язык синтаксических плюшек, которые позволят нам писать выражения как обычно, а компилятор автоматически выстраивал бы вычисления в цепочки. Теперь мы можем проводить вычисления в любом порядке, не потеряв при этом достоинств ФП (включая возможность исследовать программу математическими методами)! Если вас это сбивает с толку, то помните, что функции — это всего лишь экземпляры класса с единственным членом. Перепишите наш пример так, чтобы println и readLine были экземплярами классов, так вам станет понятней.

Но на этом польза продолжений не заканчивается. Мы можем написать всю программу целиком используя CPS, чтобы каждая функция вызывалась с дополнительным параметром, продолжением, в которое передаётся результат. В принципе любую программу можно перевести на CPS, если воспринимать каждую функцию как частный случай продолжений. Такое преобразование можно произвести автоматически (в действительности многие компиляторы так и делают).

Как только мы переведём программу к CPS виду, становится ясно, что у каждой инструкции есть продолжение, функция в которую будет передаваться результат, что в обычной программе было бы точкой вызова. Возьмём любую инструкцию из последнего примера, например add(5,10) . В программе, написанной в CPS виде, понятно что будет являться продолжением — это функция, которую add вызовет по окончанию работы. Но что будет продолжением в случае не-CPS программы? Мы, конечно, можем конвертировать программу в CPS, но нужно ли это?

Оказывается, что в этом нет необходимости. Посмотрите внимательно на наше CPS преобразование. Если вы начнёте писать компилятор для него, то обнаружите, что для CPS версии не нужен стек! Функции никогда ничего не возвращают, в традиционном понимании слова «return», они просто вызывают другую функцию, подставляя результат вычислений. Отпадает необходимость проталкивать (push) аргументы в стек перед каждым вызовом, а потом извлекать (pop) их обратно. Мы можем просто хранить аргументы в каком-либо фиксированном участке памяти и использовать jump вместо обычного вызова. Нам нет нужны хранить первоначальные аргументы, ведь они больше никогда не понадобятся, ведь функции ничего не возвращают!

Таким образом, программы в CPS стиле не нуждаются в стеке, но содержат дополнительный аргумент, в виде функции, которую нужно вызвать. Программы в не-CPS стиле лишены дополнительного аргумента, но используют стек. Что же хранится в стеке? Просто аргументы и указатель на участок памяти, куда должна вернуться функция. Ну как, вы уже догадались? В стеке храниться информация о продолжениях! Указатель на точку возврата в стеке — это то же самое, что и функция, которую нужно вызвать, в CPS программах! Чтобы выяснить, какое продолжение у add(5,10) , достаточно взять из стека точку возврата.

Это было не трудно. Продолжение и указатель на точку возврата — это действительно одно и то же, только продолжение указывается явно, и по этому оно может отличаться от того места, где функция была вызвана. Если вы помните, что продолжение — это функция, а функция в нашем языке компилируется в экземпляр класса, то поймёте, что указатель на точку возврата в стеке и указатель на продолжение — это в действительности одно и то же, ведь наша функция (как экземпляр класса) — это всего лишь указатель. А значит, что в любой момент времени в вашей программы вы можете запросить текущее продолжение (по сути информацию из стека).

Хорошо, теперь мы уяснили, что же такое текущее продолжение. Что это значит? Если мы возьмём текущее продолжение и сохраним его где-нибудь, мы тем самым сохраним текущее состояние программы — заморозим её. Это похоже на режим гибернации ОС. В объекте продолжения хранится информация, необходимая для возобновления выполнения программы с той точки, когда был запрошен объект продолжения. Операционная система постоянно так делает с вашими программами, когда переключает контекст между потоками. Разница лишь в том, что всё находится под контролем ОС. Если вы запросите объект продолжения (в Scheme это делается вызовом функции call-with-current-continuation), то вы получите объект с текущим продолжением — стеком (или в случае CPS — функцией следующего вызова). Вы можете сохранить этот объект в переменную (или даже на диск). Если вы решите «перезапустить» программу с этим продолжением, то состояние вашей программы «преобразуется» к состоянию на момент взятия объекта продолжения. Это то же самое, как переключение к приостановленному потоку, или пробуждение ОС после гибернации. С тем исключением, что вы можете проделывать это много раз подряд. После пробуждения ОС информация о гибернации уничтожается. Если этого не делать, то можно было бы восстанавливать состояние ОС с одной и той же точки. Это почти как путешествие по времени. С продолжениями вы можете себе такое позволить!

В каких ситуациях продолжения будут полезны? Обычно если вы пытаетесь эмулировать состояние в системах лишенных такового по сути. Отличное применение продолжения нашли в Web-приложениях (например во фреймворке Seaside для языка Smalltalk). ASP.NET от Microsoft прикладывает огромные усилия, чтобы сохранять состояние между запросами, и облегчить вам жизнь. Если бы C# поддерживал продолжения, то сложность ASP.NET можно было бы уменьшить в два раза — достаточно было бы сохранять продолжение и восстанавливать его при следующем запросе. С точки зрения Web-программиста не было бы ни единого разрыва — программа продолжала бы свою работу со следующей строки! Продолжения — невероятно полезная абстракция для решения некоторых проблем. Учитывая то, что всё больше и больше традиционных толстых клиентов перемещаются в Web, важность продолжений будет со временем только расти.

Сопоставление с образцом (Pattern matching)

Давайте начнём наше знакомство с Pattern matching следующим примером. Вот функция вычисления чисел Фибоначи на Java:

Int fib(int n) < if(n == 0) return 1; if(n == 1) return 1; return fib(n - 2) + fib(n - 1); >
А вот пример на Java-подобном языке с поддержкой Pattern matching-а

Int fib(0) < return 1; >int fib(1) < return 1; >int fib(int n) < return fib(n - 2) + fib(n - 1); >
В чём разница? Компилятор реализует ветвление за нас.

Подумаешь, велика важность! Действительно важность не велика. Было подмечено, что большое количество функций содержат сложные switch конструкции (это отчасти верно для функциональных программ), и было принято решение выделить этот момент. Определение функции разбивается на несколько вариантов, и устанавливается паттерн на месте аргументов функции (это напоминает перегрузку методов). Когда происходит вызов функции, компилятор на лету сравнивает аргументы со всеми определениями и выбирает наиболее подходящий. Обычно выбор падает на самое специализированное определение функции. Например int fib(int n) может быть вызвана при n равном 1, но не будет, ведь int fib(1) — более специализированное определение.

Сопоставление с образцом обычно выглядит сложнее, чем в нашем примере. Например сложная система Pattern matching позволяет писать следующий код:

Энциклопедичный YouTube

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

Математика и константы / Введение в программирование, урок 4 (JavaScript ES6)

Реактивное программирование и современные веб-интерфейсы

Александр Чирцов о математике в физике

Анна Андреева. Решение олимпиадных задач по математике

Субтитры

Языки функционального программирования

Ещё не полностью функциональные изначальные версии и Лиспа , и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang , OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Wolfram (символьная математика), и (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Лисп , созданный Джоном Маккарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма функционального программирования . Дальнейшим развитием Лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций. [ ]

Функции высших порядков

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

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

Чистые функции

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

  • Если результат чистой функции не используется, её вызов может быть удален без вреда для других выражений.
  • Результат вызова чистой функции может быть мемоизирован , то есть сохранен в таблице значений вместе с аргументами вызова. Если в дальнейшем функция вызывается с этими же аргументами, её результат может быть взят прямо из таблицы, не вычисляясь (иногда это называется принципом прозрачности ссылок). Мемоизация , ценой небольшого расхода памяти, позволяет существенно увеличить производительность и уменьшить порядок роста некоторых рекурсивных алгоритмов.
  • Если нет никакой зависимости по данным между двумя чистыми функциями, то порядок их вычисления можно поменять или распараллелить (говоря иначе вычисление чистых функций удовлетворяет принципам thread-safe)
  • Если весь язык не допускает побочных эффектов, то можно использовать любую политику вычисления. Это предоставляет свободу компилятору комбинировать и реорганизовывать вычисление выражений в программе (например, исключить древовидные структуры).

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

Рекурсивные функции можно обобщить с помощью функций высших порядков, используя, например, катаморфизм и анаморфизм (или «свертка» и «развертка»). Функции такого рода играют роль такого понятия как цикл в императивных языках программирования. [ ]

Подход к вычислению аргументов

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

print (len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 — 4 ]))

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell . [ ]

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на Python. Другим примером является язык Ruby , который имеет возможность создания как lambda-объектов, так и возможность организации анонимных функций высшего порядка через блок с помощью конструкции yield.

Стили программирования

Императивные программы имеют склонность акцентировать последовательности шагов для выполнения какого-то действия, а функциональные программы к расположению и композиции функций, часто не обозначая точной последовательности шагов. Простой пример двух решений одной задачи (используется один и тот же язык Python) иллюстрирует это.

# императивный стиль target = # создать пустой список for item in source_list : # для каждого элемента исходного списка trans1 = G (item ) # применить функцию G() trans2 = F (trans1 ) # применить функцию F() target . append (trans2 ) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A , B : lambda x : A (B (x )) target = map (compose2 (F , G ), source_list )

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

Более точно, существует четыре ступени развития функционального стиля, в порядке убывания роли данных в программах:

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп , , Tcl , Rebol);
  • Комбинаторные (APL / / , FP / FL );
  • Бесточечные (чистые конкатенативные) (Joy , Cat , Factor , подмножество PostScript).
Мастер Йода рекомендует:  Лучшие бесплатные плагины WordPress за октябрь 2020 года

В первом случае вся структура программы определяется структурой данных, в последнем — данные как таковые вообще отсутствуют в исходном коде, они лишь подразумеваются на входе. Некоторые языки поддерживают ряд стилей: например, Haskell позволяет писать и в аппликативном, и в комбинаторном, и в бесточечном стилях.

Особенности

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

Сильные стороны

Повышение надёжности кода

Привлекательная сторона вычислений без состояний — повышение надёжности кода за счёт чёткой структуризации и отсутствия необходимости отслеживания побочных эффектов. Любая функция работает только с локальными данными и работает с ними всегда одинаково, независимо от того, где, как и при каких обстоятельствах она вызывается. Невозможность мутации данных при пользовании ими в разных местах программы исключает появление труднообнаруживаемых ошибок (таких, например, как случайное присваивание неверного значения глобальной переменной в императивной программе).

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

Недостатки функционального программирования вытекают из тех же самых его особенностей. Отсутствие присваиваний и замена их на порождение новых данных приводят к необходимости постоянного выделения и автоматического освобождения памяти, поэтому в системе исполнения функциональной программы обязательным компонентом становится высокоэффективный сборщик мусора . Нестрогая модель вычислений приводит к непредсказуемому порядку вызова функций, что создает проблемы при вводе-выводе, где порядок выполнения операций важен. Кроме того, очевидно, функции ввода в своем естественном виде (например, getchar из стандартной библиотеки языка ) не являются чистыми, поскольку способны возвращать различные значения для одних и тех же аргументов, и для устранения этого требуются определенные ухищрения.

Для преодоления недостатков функциональных программ уже первые языки функционального программирования включали не только чисто функциональные средства, но и механизмы императивного программирования (присваивание, цикл, «неявный PROGN» были уже в Лиспе). Использование таких средств позволяет решить некоторые практические проблемы, но означает отход от идей (и преимуществ) функционального программирования и написание императивных программ на функциональных языках. В чистых функциональных языках эти проблемы решаются другими средствами, например, в языке Haskell ввод-вывод реализован при помощи монад — нетривиальной концепции, позаимствованной из теории категорий.

String reverse(String arg) < if(arg.length == 0) < return arg; >else < return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); >>
Эта функция довольно медленная, потому что она повторно вызывает сама себя . Здесь возможна утечка памяти, так как множество раз создаются временные объекты. Но это функциональный стиль. Вам может показать странным, как люди могут так программировать. Ну, я как раз собирался вам рассказать.

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

Unit тестирование

Вот она, голубая мечта unit-тестеров. Можно протестировать каждую функцию в программе используя только нужные аргументы. Нет необходимости вызывать функции в правильном порядке или воссоздавать правильное внешнее состояние. Всё что вам нужно, это передать аргументы, которые соответствуют граничным случаям. Если все функции в вашей программе проходят Unit-тесты, то вы можете быть намного более уверены в качестве вашего ПО, чем в случае императивных языков программирования. В Java или C++ проверки возвращаемого значения не достаточно — функция может поменять внешнее состояние, которое тоже подлежит проверке. В ФП такой проблемы нет.

Отладка

Как только вы воспроизведёте ошибку, найти её источник — тривиальная задача. Это даже приятно. Как только вы остановите выполнение программы, перед вами будет весь стек вызовов. Вы можете просмотреть аргументы вызова каждой функции, прямо как в императивном языке. С тем отличием, что в императивной программе этого не достаточно, ведь функции зависят от значений полей, глобальных переменных и состояний других классов. Функция в ФП зависит только от своих аргументов, и эта информация оказывается прямо у вас перед глазами! Даже больше, в императивной программе проверки возвращаемого значения не достаточно для того, чтобы сказать, правильно ли ведёт себя кусок кода. Вам придётся выследить десятки объектов за пределами функции, чтобы удостовериться, что всё работает правильно. В функциональном программировании всё, что нужно сделать — это взглянуть на возвращаемое значение!

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

Многопоточность

Если дела обстоят подобным образом, то почему так редко функциональные языки программирования используются в многопоточных приложениях? На самом деле чаще, чем вы думаете. Компания Ericsson разработала функциональный язык под названием Erlang для использования на отказоустойчивых и масштабируемых телекоммуникационных коммутаторах. Многие отметили преимущества Erlang-а и стали его использовать . Мы говорим о телекоммуникациях и системах контроля трафика, которые далеко не так просто масштабируются, как типичные системы, разработанные на Wall Street. Вообще-то, системы написанные на Erlang, не такие масштабируемые и надёжные, как Java системы. Erlang системы просто сверхнадёжные.

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

Компилятор функционального языка может проанализировать код, классифицировать функции, которые создают строки s1 и s2 , как функции потребляющие много времени, и запустить их параллельно. Это невозможно сделать в императивном языке, потому что каждая функция может изменять внешнее состояние и код, идущий непосредственно после вызова, может зависеть от неё. В ФП автоматический анализ функций и поиск подходящих кандидатов для распараллеливания — это тривиальнейшая задача, как автоматический inline ! В этом смысле функциональный стиль программирования соответствует требованиям завтрашнего дня. Разработчики железа уже не могут заставить CPU работать быстрее. Вместо этого они наращивают количество ядер и заявляют о четырёхкратном увеличении скорости многопоточных вычислений. Конечно они очень вовремя забывают сказать, что ваш новый процессор покажет прирост только в программах, разработанных с учётом распараллеливания. Среди императивного ПО таких очень мало. Зато 100% функциональных программ готовы к многопоточности из коробки.

Развёртывание по горячему

В идеале нужно обновить все нужные участки кода не останавливая систему в принципе. В императивном мире это невозможно [пер. в Smalltalk-е очень даже возможно]. Представьте себе выгрузку Java класса на лету и перезагрузка новой версии. Если бы мы так сделали, то все экземпляры класса стали бы нерабочими, потому что потерялось бы состояние, которое они хранили. Нам пришлось бы писать хитрый код, для контроля версий. Пришлось бы серриализовать все созданные экземпляры класса, потом уничтожить их, создать экземпляры нового класса, попытаться загрузить серриализованные данные в надежде, что миграция пройдёт нормально и новые экземпляры будут валидными. И кроме того, миграционный код необходимо писать каждый раз вручную. И ещё миграционный код должен сохранять ссылки между объектами. В теории ещё куда ни шло, но на практике это никогда не заработает.

В функциональной программе всё состояние хранится в стеке в виде аргументов функций. Это позволяет значительно упростить развёртывание по горячему! По сути всё что нужно сделать — это вычислить разницу между кодом на рабочем сервере и новой версией, и установить изменения в коде. Остальное будет сделано языковыми инструментами автоматически! Если вы думаете, что это научная фантастика, то дважды подумайте. Инженеры, имеющие дело с Erlang, годами обновляют свои системы без остановки их работы.

Доказательные вычисления и оптимизация (Machine Assisted Proofs and Optimizations)

Дополнительно вы можете использовать математический аппарат, чтобы доказать корректность участков ваших программ. При желании можно написать инструменты, которые анализируют код и автоматически создают Unit-тесты для граничных случаев! Такая функциональность бесценна для сверхнадёжных систем (rock solid systems). При разработке систем контроля кардиостимуляторов или управления воздушным трафиком такие инструменты просто необходимы. Если же ваши разработки не находятся в сфере критически важных приложений, то инструменты автоматической проверки всё равно дадут вам гигантское преимущество перед вашими конкурентами.

Функции высшего порядка

В ФП функция — это не тоже самое, что функция в Java или C. Это надмножество — они могут тоже самое, что Java функции и даже больше. Пусть у нас есть функция на C:

Int add(int i, int j) < return i + j; >
В ФП это не тоже самое, что обычная C функция. Давайте расширим наш Java компилятор, чтобы он поддерживал такую запись. Компилятор должен превратить объявление функции в следующий Java код (не забывайте, что везде присутствует неявный final):

> Символ add не совсем функция. Это маленький класс с одним методом. Теперь мы можем передавать add в качестве аргумента в другие функции. Мы можем записать его в другой символ. Мы можем создавать экземпляры add_function_t в runtime и они будут уничтожены сборщиком мусора, если станут ненужными. Функции становятся базовыми объектами, как числа и строки. Функции, которые оперируют функциями (принимают их в качестве аргументов) называются функциями высшего порядка. Пусть это вас не пугает. Понятие функций высшего порядка почти не отличается от понятия Java классов, которые оперируют друг другом (мы можем передавать классы в другие классы). Мы можем называть их «классы высшего порядка», но никто этим не заморачивается, потому что за Java не стоит строгое академическое сообщество.

Как и когда нужно использовать функции высшего порядка? Я рад, что вы спросили. Вы пишите свою программу как один большой монолитный кусок кода не заботясь об иерархии классов. Если вы увидите, что какой-то участок кода повторяется в разных места, вы выносите его в отдельную функцию (к счастью в школах еще учат как это делать). Если вы замечаете, что часть логики в вашей функции должна вести себя по разному в некоторых ситуациях, то вы создаёте функцию высшего порядка. Запутались? Вот реальный пример из мой работы.

Предположим, что у нас есть участок Java кода, который получает сообщение, преобразует его различными способами и передаёт на другой сервер.

Void handleMessage(Message msg) < // . msg.setClientCode("ABCD_123"); // . sendMessage(msg); >// . >
Теперь представьте себе, что система поменялась, и теперь нужно распределять сообщения между двумя серверами вместо одного. Всё остаётся неизменным, кроме кода клиента — второй сервер хочет получать этот код в другом формате. Как нам справиться с этой ситуацией? Мы можем проверять, куда должно попасть сообщение, и в зависимости от этого устанавливать правильный код клиента. Например так:

Class MessageHandler < void handleMessage(Message msg) < // . if(msg.getDestination().equals("server1") < msg.setClientCode("ABCD_123"); >else < msg.setClientCode("123_ABC"); >// . sendMessage(msg); > // . >
Но такой подход плохо масштабируется. При добавлении новых серверов функция будет расти линейно, и внесение изменений превратится в кошмар. Объектно ориентированный подход заключается в выделении общего суперкласса MessageHandler и вынесение логики определения кода клиента в подклассы:

Abstract class MessageHandler < void handleMessage(Message msg) < // . msg.setClientCode(getClientCode()); // . sendMessage(msg); >abstract String getClientCode(); // . > class MessageHandlerOne extends MessageHandler < String getClientCode() < return "ABCD_123"; >> class MessageHandlerTwo extends MessageHandler < String getClientCode() < return "123_ABCD"; >>
Теперь для каждого сервера мы можем создать экземпляр соответствующего класса. Добавление новых сервером становится более удобным. Но для такого небольшого изменения многовато текста. Пришлось создать два новых типа чтобы просто добавить поддержку различного кода клиента! Теперь сделаем тоже самое в нашем языке с поддержкой функций высшего порядка:

; > MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
Мы не создавали новых типов и не усложняли иерархию классов. Мы просто передали функцию в качестве параметра. Мы достигли того же эффекта, как и в объектно-ориентированном аналоге, только с некоторыми преимуществами. Мы не привязывали себя к какой-либо иерархии классов: мы можем передавать любые другие функции в runtime и менять их в любой момент, сохраняя при этом высокий уровень модульности меньшим количеством кода. По сути компилятор создал объектно-ориентированный «клей» вместо нас! При этом сохраняются все остальные преимущества ФП. Конечно абстракции, предлагаемые функциональными языками на этом не заканчиваются. Функции высшего порядка это только начало

Каррирование

Функциональные языки невероятно выразительны. В функциональном языке вам не понадобятся паттерны проектирования, потому что язык настолько высокоуровневый, что вы легко начнёте программировать в концепциях, которые исключают все известные паттерны программирования. Одним из таких паттернов является Адаптер (чем он отличается от Фасада? Похоже, что кому-то понадобилось наштамповать побольше страниц, чтобы выполнить условия контракта). Этот паттерн оказывается ненужным если в языке есть поддержка каррирования.

Паттерн Адаптер наиболее часто применяется к «стандартной» единице абстракции в Java — классу. В функциональных языках паттерн применяется к функциям. Паттерн берёт интерфейс и преобразует его в другой интерфейс, согласно определённым требованиям. Вот пример паттерна Адаптер:

Int pow(int i, int j); int square(int i) < return pow(i, 2); >
Этот код адаптирует интерфейс функции, возводящей число в произвольную степень, к интерфейсу функции, которая возводит число в квадрат. В аккадемических кругах этот простейший приём называется каррирование (в честь специалиста по логике Хаскелла Карри (Haskell Curry), который провёл ряд математических трюков, чтобы всё это формализовать). Так как в ФП функции используются повсеместно в качестве аргументов, каррирование используется очень часто, чтобы привести функции к интерфейсу, необходимому в том или ином месте. Так как интерфейс функции — это её аргументы, то каррирование используется для уменьшения количества аргументов (как в примере выше).

Этот инструмент является встроенным в функциональные языки. Вам не нужно вручную создавать функцию, которая оборачивает оригинал. Функциональный язык сделает всё за вас. Как обычно давайте расширим наш язык, добавив в него каррирование.

Square = int pow(int i, 2);
Этой строкой мы автоматически создаём функцию возведения в квадрат с одним аргументом. Новая функция будет вызывать функцию pow , подставляя 2 в качестве второго аргумента. С точки зрения Java, это будет выглядеть следующим образом:

> Как видите, мы просто написали обёртку над оригинальной функцией. В ФП каррирование как раз и представляет из себя простой и удобный способ создания обёрток. Вы сосредотачиваетесь на задаче, а компилятор пишет необходимый код за вас! Всё очень просто, и происходит каждый раз, когда вы хотите использовать паттерн Адаптер (обёртку).

Ленивые вычисления

String s1 = somewhatLongOperation1(); String s2 = somewhatLongOperation2(); String s3 = concatenate(s1, s2);
В императивных языках программирования очерёдность вычисления не вызывает никаких вопросов. Поскольку каждая функция может повлиять или зависеть от внешнего состояния, то необходимо соблюдать чёткую очерёдность вызовов: сначала somewhatLongOperation1 , затем somewhatLongOperation2 , и concatenate в конце. Но не всё так просто в функциональных языках.

Как мы уже видели ранее somewhatLongOperation1 и somewhatLongOperation2 могут быть запущены одновременно, потому что функции гарантированно не влияют и не зависят от глобального состояния. Но что, если мы не хотим выполнять их одновременно, нужно ли вызывать их последовательно? Ответ — нет. Эти вычисления должны быть запущены, только если какая-либо другая функция зависит от s1 и s2 . Нам даже не нужно выполнять их до тех пор, пока они понадобятся внутри concatenate . Если вместо concatenate мы подставим функцию, которая в зависимости от условия использует один аргумент из двух, то второй аргумент можно даже не вычислять! Haskell — это пример языка с отложенными вычислениями. В Haskell отсутствует гарантия какой-либо очередности вызовов (вообще!), потому что Haskell выполняет код по мере необходимости.

Ленивые вычисления обладают рядом достоинств как и некоторыми недостатками. В следующем разделе мы обсудим достоинства и я объясню как уживаться с недостатками.

Оптимизация

Абстрагирование структур управления

Unless(stock.isEuropean()) < sendToSEC(stock); >
Мы хотим, чтобы функция sendToSEC выполнялась только если фонд (stock) не европейский. Как можно реализовать unless ? Без ленивый вычислений нам бы понадобилась система макросов, но в языках, подобных Haskell, это не обязательно. Мы можем объявить unless в виде функции!

Void unless(boolean condition, List code) < if(!condition) code; >
Заметьте, что code не будет выполняться, если condition == true . В строгих языках такое поведение невозможно повторить, так как аргументы будут вычислены прежде, чем unless будет вызвана.

Бесконечные структуры данных

Недостатки

В ленивом языке никто не гарантирует, что первая строка выполнится раньше второй! Это означает, что мы не можем делать ввод-вывод, не можем нормально использовать нативные функции (ведь их нужно вызывать в определённом порядке, чтобы учитывать их побочные эффекты), и не можем взаимодействовать с внешним миром! Если мы введём механизм для упорядочивания выполнения кода, то потеряем преимущество математической строгости кода (а следом потеряем все плюшки функционального программирования). К счастью ещё не всё потеряно. Математики взялись за работу и придумали несколько приёмов для того, чтобы убедится в правильном порядке выполняемых инструкций не потеряв функционального духа. Мы получили лучшее от двух миров! Такие приёмы включают в себя продолжения (continuation), монады (monads) и однозначная типизация (uniqueness typing). В данной статье мы поработаем с продолжениями, а монады и однозначную типизацию отложим до следующего раза. Занятно, что продолжения очень полезная штука, которая используется не только для задания строгого порядка вычислений. Об этом мы тоже поговорим.

Продолжения

Когда мы рассматривали функции, мы изучили лишь половину правды, ведь мы исходили из предположения, что функция возвращает значение в вызывающую её функцию. В этом смысле продолжение — это обобщение функций. Функция не обязательно должна возвращать управление в то место, откуда её вызвали, а может возвращать в любое место программы. «Продолжение» — это параметр, который мы можем передать в функцию, чтобы указать точку возврата. Звучит намного страшнее, чем есть на самом деле. Давайте взглянем на следующий код:

Int i = add(5, 10); int j = square(i);
Функция add возвращает число 15, которое записывается в i , в том месте, где функция и была вызвана. Затем значение i используется при вызове square . Заметьте, что ленивый компилятор не может поменять очередность вычислений, ведь вторая строка зависит от результата первой. Мы можем переписать этот код с использованием Стиль Передачи Продолжения (Continuation Passing Style или CPS), когда add возвращает значение в функцию square .

Int j = add(5, 10, square);
В таком случае add получает дополнительный аргумент — функцию, которая будет вызвана после того, как add закончит работать. В обоих примерах j будет равен 225.

В этом и заключается первый приём, позволяющий задать порядок выполнения двух выражений. Вернёмся к нашему примеру с вводом-выводом

System.out.println(«Please enter your name: «); System.in.readLine();
Эти две строки не зависят друг от друга, и компилятор волен поменять их порядок по своему хотению. Но если мы перепишем в CPS, то тем самым добавим нужную зависимость, и компилятору придётся проводить вычисления одно за другим!

System.out.println(«Please enter your name: «, System.in.readLine);
В таком случае println должен будет вызвать readLine , передав ему свой результат, и вернуть результат readLine в конце. В таком виде мы можем быть уверены, что эти функции будут вызваны по очереди, и что readLine вообще вызовется (ведь компилятор ожидает получить результат последней операции). В случае Java println возвращает void . Но если бы возвращалось какое-либо абстрактное значение (которое может служить аргументом readLine), то это решило бы нашу проблему! Конечно выстраивание таких цепочек функций сильно ухудшает читаемость кода, но с этим можно бороться. Мы можем добавить в наш язык синтаксических плюшек, которые позволят нам писать выражения как обычно, а компилятор автоматически выстраивал бы вычисления в цепочки. Теперь мы можем проводить вычисления в любом порядке, не потеряв при этом достоинств ФП (включая возможность исследовать программу математическими методами)! Если вас это сбивает с толку, то помните, что функции — это всего лишь экземпляры класса с единственным членом. Перепишите наш пример так, чтобы println и readLine были экземплярами классов, так вам станет понятней.

Но на этом польза продолжений не заканчивается. Мы можем написать всю программу целиком используя CPS, чтобы каждая функция вызывалась с дополнительным параметром, продолжением, в которое передаётся результат. В принципе любую программу можно перевести на CPS, если воспринимать каждую функцию как частный случай продолжений. Такое преобразование можно произвести автоматически (в действительности многие компиляторы так и делают).

Как только мы переведём программу к CPS виду, становится ясно, что у каждой инструкции есть продолжение, функция в которую будет передаваться результат, что в обычной программе было бы точкой вызова. Возьмём любую инструкцию из последнего примера, например add(5,10) . В программе, написанной в CPS виде, понятно что будет являться продолжением — это функция, которую add вызовет по окончанию работы. Но что будет продолжением в случае не-CPS программы? Мы, конечно, можем конвертировать программу в CPS, но нужно ли это?

Оказывается, что в этом нет необходимости. Посмотрите внимательно на наше CPS преобразование. Если вы начнёте писать компилятор для него, то обнаружите, что для CPS версии не нужен стек! Функции никогда ничего не возвращают, в традиционном понимании слова «return», они просто вызывают другую функцию, подставляя результат вычислений. Отпадает необходимость проталкивать (push) аргументы в стек перед каждым вызовом, а потом извлекать (pop) их обратно. Мы можем просто хранить аргументы в каком-либо фиксированном участке памяти и использовать jump вместо обычного вызова. Нам нет нужны хранить первоначальные аргументы, ведь они больше никогда не понадобятся, ведь функции ничего не возвращают!

Таким образом, программы в CPS стиле не нуждаются в стеке, но содержат дополнительный аргумент, в виде функции, которую нужно вызвать. Программы в не-CPS стиле лишены дополнительного аргумента, но используют стек. Что же хранится в стеке? Просто аргументы и указатель на участок памяти, куда должна вернуться функция. Ну как, вы уже догадались? В стеке храниться информация о продолжениях! Указатель на точку возврата в стеке — это то же самое, что и функция, которую нужно вызвать, в CPS программах! Чтобы выяснить, какое продолжение у add(5,10) , достаточно взять из стека точку возврата.

Это было не трудно. Продолжение и указатель на точку возврата — это действительно одно и то же, только продолжение указывается явно, и по этому оно может отличаться от того места, где функция была вызвана. Если вы помните, что продолжение — это функция, а функция в нашем языке компилируется в экземпляр класса, то поймёте, что указатель на точку возврата в стеке и указатель на продолжение — это в действительности одно и то же, ведь наша функция (как экземпляр класса) — это всего лишь указатель. А значит, что в любой момент времени в вашей программы вы можете запросить текущее продолжение (по сути информацию из стека).

Хорошо, теперь мы уяснили, что же такое текущее продолжение. Что это значит? Если мы возьмём текущее продолжение и сохраним его где-нибудь, мы тем самым сохраним текущее состояние программы — заморозим её. Это похоже на режим гибернации ОС. В объекте продолжения хранится информация, необходимая для возобновления выполнения программы с той точки, когда был запрошен объект продолжения. Операционная система постоянно так делает с вашими программами, когда переключает контекст между потоками. Разница лишь в том, что всё находится под контролем ОС. Если вы запросите объект продолжения (в Scheme это делается вызовом функции call-with-current-continuation), то вы получите объект с текущим продолжением — стеком (или в случае CPS — функцией следующего вызова). Вы можете сохранить этот объект в переменную (или даже на диск). Если вы решите «перезапустить» программу с этим продолжением, то состояние вашей программы «преобразуется» к состоянию на момент взятия объекта продолжения. Это то же самое, как переключение к приостановленному потоку, или пробуждение ОС после гибернации. С тем исключением, что вы можете проделывать это много раз подряд. После пробуждения ОС информация о гибернации уничтожается. Если этого не делать, то можно было бы восстанавливать состояние ОС с одной и той же точки. Это почти как путешествие по времени. С продолжениями вы можете себе такое позволить!

В каких ситуациях продолжения будут полезны? Обычно если вы пытаетесь эмулировать состояние в системах лишенных такового по сути. Отличное применение продолжения нашли в Web-приложениях (например во фреймворке Seaside для языка Smalltalk). ASP.NET от Microsoft прикладывает огромные усилия, чтобы сохранять состояние между запросами, и облегчить вам жизнь. Если бы C# поддерживал продолжения, то сложность ASP.NET можно было бы уменьшить в два раза — достаточно было бы сохранять продолжение и восстанавливать его при следующем запросе. С точки зрения Web-программиста не было бы ни единого разрыва — программа продолжала бы свою работу со следующей строки! Продолжения — невероятно полезная абстракция для решения некоторых проблем. Учитывая то, что всё больше и больше традиционных толстых клиентов перемещаются в Web, важность продолжений будет со временем только расти.

Сопоставление с образцом (Pattern matching)

Давайте начнём наше знакомство с Pattern matching следующим примером. Вот функция вычисления чисел Фибоначи на Java:

Int fib(int n) < if(n == 0) return 1; if(n == 1) return 1; return fib(n - 2) + fib(n - 1); >
А вот пример на Java-подобном языке с поддержкой Pattern matching-а

Int fib(0) < return 1; >int fib(1) < return 1; >int fib(int n) < return fib(n - 2) + fib(n - 1); >
В чём разница? Компилятор реализует ветвление за нас.

Подумаешь, велика важность! Действительно важность не велика. Было подмечено, что большое количество функций содержат сложные switch конструкции (это отчасти верно для функциональных программ), и было принято решение выделить этот момент. Определение функции разбивается на несколько вариантов, и устанавливается паттерн на месте аргументов функции (это напоминает перегрузку методов). Когда происходит вызов функции, компилятор на лету сравнивает аргументы со всеми определениями и выбирает наиболее подходящий. Обычно выбор падает на самое специализированное определение функции. Например int fib(int n) может быть вызвана при n равном 1, но не будет, ведь int fib(1) — более специализированное определение.

Сопоставление с образцом обычно выглядит сложнее, чем в нашем примере. Например сложная система Pattern matching позволяет писать следующий код:

Элементы функционального программирования. Принципы функционального программирования: почему это важно

Функциональное программирование предполагает обходиться вычислением результатов функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы. Соответственно, не предполагает оно и изменяемость этого состояния (в отличие от императивного , где одной из базовых концепций является переменная , хранящая своё значение и позволяющая менять его по мере выполнения алгоритма).

На практике отличие математической функции от понятия «функции» в императивном программировании заключается в том, что императивные функции могут опираться не только на аргументы, но и на состояние внешних по отношению к функции переменных, а также иметь побочные эффекты и менять состояние внешних переменных. Таким образом, в императивном программировании при вызове одной и той же функции с одинаковыми параметрами, но на разных этапах выполнения алгоритма, можно получить разные данные на выходе из-за влияния на функцию состояния переменных. А в функциональном языке при вызове функции с одними и теми же аргументами мы всегда получим одинаковый результат: выходные данные зависят только от входных. Это позволяет средам выполнения программ на функциональных языках кешировать результаты функций и вызывать их в порядке, не определяемом алгоритмом и распараллеливать их без каких-либо дополнительных действий со стороны программиста (см.ниже )

Языки функционального программирования

  • LISP — (Джон МакКарти , ) и множество его диалектов, наиболее современные из которых:
  • Erlang — (Joe Armstrong, ) функциональный язык с поддержкой процессов.
  • APL — предшественник современных научных вычислительных сред, таких как MATLAB .
  • (Робин Милнер , , из ныне используемых диалектов известны Standard ML и Objective CAML).
  • — функциональный язык семейства ML для платформы .NET
  • Miranda (Дэвид Тёрнер , , который впоследствии дал развитие языку Haskell).
  • Nemerle — гибридный функционально/императивный язык.
  • Haskell — чистый функциональный. Назван в честь Хаскелла Карри .

Ещё не полностью функциональные изначальные версии и Lisp и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang, OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Mathematica (символьная математика), и K (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Lisp , созданный Джоном МакКарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . Lisp ввел множество понятий функционального языка, хотя при этом исповедовал не только парадигму функционального программирования . Дальнейшим развитием лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций.

Функции высших порядков

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

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

Чистые функции

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

  • Если результат чистой функции не используется, он может быть удален без вреда для других выражений.
  • Результат вызова чистой функции может быть мемоизирован , то есть сохранен в таблице значений вместе с аргументами вызова. Если в дальнейшем функция вызывается с этими же аргументами, ее результат может быть взят прямо из таблицы, не вычисляясь (иногда это называется принципом прозрачности ссылок). Мемоизация , ценой небольшого расхода памяти, позволяет существенно увеличить производительность и уменьшить порядок роста некоторых рекурсивных алгоритмов.
  • Если нет никакой зависимости по данным между двумя чистыми функциями, то порядок их вычисления можно поменять или распараллелить (говоря иначе вычисление чистых функций удовлетворяет принципам thread-safe)
  • Если весь язык не допускает побочных эффектов, то можно использовать любую политику вычисления. Это предоставляет свободу компилятору комбинировать и реорганизовывать вычисление выражений в программе (например, исключить древовидные структуры).

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

Рекурсивные функции можно обобщить с помощью функций высших порядков, используя, например, катаморфизм и анаморфизм (или «свертка» и «развертка»). Функции такого рода играют роль такого понятия как цикл в императивных языках программирования.

Подход к вычислению аргументов

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

Print (len ([ 2 +1 , 3 *2 , 1 /0 , 5 -4 ] ) )

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell .

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на языке Python .

Стили программирования

Императивные программы имеют склонность акцентировать последовательности шагов для выполнения какого-то действия, а функциональные программы к расположению и композиции функций, часто не обозначая точной последовательности шагов. Простой пример двух решений одной задачи (используется один и тот же язык Python) иллюстрирует это.

# императивный стиль target = # создать пустой список for item in source_list: # для каждого элемента исходного списка trans1 = G(item) # применить функцию G() trans2 = F(trans1) # применить функцию F() target.append (trans2) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A, B: lambda x: A(B(x) ) target = map (compose2(F, G) , source_list)

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

Особенности

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

Сильные стороны

Повышение надёжности кода

Привлекательная сторона вычислений без состояний — повышение надёжности кода за счёт чёткой структуризации и отсутствия необходимости отслеживания побочных эффектов. Любая функция работает только с локальными данными и работает с ними всегда одинаково, независимо от того, где, как и при каких обстоятельствах она вызывается. Невозможность мутации данных при пользовании ими в разных местах программы исключает появление труднообнаруживаемых ошибок (таких, например, как случайное присваивание неверного значения глобальной переменной в императивной программе).

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

Недостатки функционального программирования вытекают из тех же самых его особенностей. Отсутствие присваиваний и замена их на порождение новых данных приводят к необходимости постоянного выделения и автоматического освобождения памяти, поэтому в системе исполнения функциональной программы обязательным компонентом становится высокоэффективный сборщик мусора . Нестрогая модель вычислений приводит к непредсказуемому порядку вызова функций, что создает проблемы при вводе-выводе, где порядок выполнения операций важен. Кроме того, очевидно, функции ввода в своем естественном виде (например, getchar из стандартной библиотеки языка ) не являются чистыми, поскольку способны возвращать различные значения для одних и тех же аргументов, и для устранения этого требуются определенные ухищрения.

Для преодоления недостатков функциональных программ уже первые языки функционального программирования включали не только чисто функциональные средства, но и механизмы императивного программирования (присваивание, цикл, «неявный PROGN» были уже в LISPе). Использование таких средств позволяет решить некоторые практические проблемы, но означает отход от идей (и преимуществ) функционального программирования и написание императивных программ на функциональных языках. В чистых функциональных языках эти проблемы решаются другими средствами, например, в языке Haskell ввод-вывод реализован при помощи монад — нетривиальной концепции, позаимствованной из теории категорий.

См. также

  • Анаморфизм
  • Катаморфизм

Примечания

  1. А. Филд, П. Харрисон Функциональное программирование: Пер. с англ. — М.: Мир, 1993. — 637 с, ил. ISBN 5-03-001870-0 . Стр. 120 [Глава 6: Математические основы: λ-исчисление].
  2. Tiobe Programming Community Index
  3. Пол Хьюдак (англ.) русск. (September 1989). «Conception, evolution, and application of functional programming languages » (PDF). ACM Computing Surveys21 (3): 359-411. DOI :10.1145/72551.72554 .
  4. Роджер Пенроуз Глава 2: Лямбда-исчисление Черча // Новый ум короля. О компьютерах, мышлении и законах физики = The Emperors New Mind: Concerning Computers, Minds and The Laws of Physics. — Едиториал УРСС, 2003. — ISBN 5-354-00005-X + переиздание ISBN 978-5-382-01266-7 ; 2011 г.
  5. McCarthy, John (June 1978). «History of Lisp ». In ACM SIGPLAN History of Programming Languages Conference : 217–223. DOI :10.1145/800025.808387 .
  6. , Гл. 3. λ-исчисление как язык программирования
  7. В своих мемуарах Герберт Саймон (1991), Models of My Life pp.189-190 ISBN 0-465-04640-1 утверждает, что его, Al. Ньюэлл, и Клифф Шоу которых «часто называют родителями искусственного интеллекта» за написание программы Logic Theorist (англ.) русск. автоматически доказывающей теоремы из Principia Mathematica (англ.) русск. . Для того, чтобы достичь этого, они должны были придумать язык и парадигму, которую, ретроспективно, можно рассматривать как функциональное программирование.
  8. History of Programming Languages: IPL
  9. XIV. APL Session // History of Programming Language / Richard L. Wexelbblat. — Academic Press, 1981. — С. 661-693. — 749 с.
  10. Скачать PDF: «Техники функционального программирования, В. А. Потапенко» стр. 8 «Функции высших порядков» .
  11. GCC, Declaring Attributes of Functions
  12. XL Fortran for AIX, V13.1 > Language Reference, Pure procedures (Fortran 95)
  13. Tail call optimization

Язык функционального программирования

В качестве основных свойств функциональных языков программирования обычно рассматриваются [кем? ] следующие:

Программы на функциональных языках обычно намного короче и проще, чем те же самые программы на императивных языках.
Пример (быстрая сортировка Хоара на абстрактном функциональном языке) :

QuickSort () =
quickSort () = quickSort (n | n t, n h)

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

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

В функциональных языках (равно как и вообще в языках программирования и математике) функции могут быть переданы другим функциям в качестве аргумента или возвращены в качестве результата. Функции, принимающие функциональные аргументы, называются функциями высших порядков или функционалами.

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

В традиционных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. В каком-то смысле противоположностью вызова-по-значению является вызов-по-необходимости (ленивые вычисления). В этом случае аргумент вычисляется, только если он нужен для вычисления результата.

Некоторые языки функционального программирования

Классификация функциональных языков

В качестве примера чистого функционального языка можно привести Haskell . Однако большинство функциональных языков являются гибридными и содержат свойства как функциональных, так и императивных языков. Яркие примеры — языки Scala и Nemerle. В них органично сочетаются характеристики как объектно-ориентированных языков, так и функциональных. Реализована хвостовая рекурсия и её оптимизация, функция является полноправным объектом, то есть может быть сохранена в переменной, передана в качестве аргумента в другую функцию или возвращена из функции.

Также функциональные языки делят на строгие и нестрогие . К нестрогим языкам относят те, которые поддерживают отложенные вычисления (F#), то есть аргументы функции вычисляются только тогда, когда они действительно понадобятся при вычислении функции. Ярким примером нестрогого языка является Haskell. В качестве примера строгого языка можно привести Standard ML .

Некоторые функциональные языки реализованы поверх платформообразующих виртуальных машин (JVM, .NET), то есть приложения на этих языках могут работать в среде времени исполнения (JRE, CLR) и использовать встроенные классы. К ним относятся Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

Ссылки

  • https://fprog.ru/ — Журнал «Практика функционального программирования»
  • https://www.intuit.ru/department/pl/funcpl/1/ — Основы функционального программирования. Л. В. Городняя
  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года;
  • https://alexott.net/ru/fp/books/ — Обзор литературы о функциональном программировании . Рассматриваются книги как на русском, так и на английском языке.

Wikimedia Foundation . 2010 .

Смотреть что такое «Язык функционального программирования» в других словарях:

язык прграммирования Лисп — Язык функционального программирования. Тематики информационные технологии в целом EN Lisp … Справочник технического переводчика

Универсальный язык программирования высокого уровня. Язык Лисп: относится к декларативным языкам функционального типа; предназначен для обработки символьных данных, представленных в виде списков. Основой языка являются функции и рекурсивные… … Финансовый словарь

У этого термина существуют и другие значения, см. Alice. Alice Семантика: функциональный Тип исполнения: компиляция в байткод для виртуальной машины Появился в: 2002 … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Oz Семантика: функциональный, процедурный, декларативный, объектно ориентированный, вычисления с ограничениями, Н модели, параллельные вычисления Тип исполнения: компилируемый Появился в: 1991 Автор(ы): Gert Smolka his students Релиз … Википедия

AWL (Alternative Web Language) Класс языка: мультипарадигмальный: функциональный, процедурный, объектно ориентированный Тип исполнения: интерпретируемый Появился в: 2005 г. Типизация данных: динамическая … Википедия

У этого термина существуют и другие значения, см. Леда (значения). Леда (Leda) мультипарадигмальный язык программирования, спроектированный Тимоти Баддом. Язык Leda исходно создавался с целью совмещения императивного программирования, объектно… … Википедия

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

В языках функционального программирования основным конструктивным элементом является математическое понятие функции. Существует различия в понимании функции в математике и функции в программировании, в следствии чего нельзя отнести Си подобные… … Википедия

Python был задуман в 1980 х годах, а его создание началось в декабре 1989 года Гвидо ван Россумом в составе центра математики и информатики в Нидерландах. Язык Python был задуман как потомок языка программирования ABC, способный к обработке… … Википедия

Функции являются абстракциями , в которых детали реализации некоторого действия скрываются за отдельным именем. Хорошо написанный набор функций позволяет использовать их много раз. Стандартная библиотека Python содержит множество готовых и отлаженных функций, многие из которых достаточно универсальны, чтобы работать с широким спектром входных данных. Даже если некоторый участок кода не используется несколько раз, но по входным и выходным данным он достаточно автономен, его смело можно выделить в отдельную функцию.

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

Далее будут подробно рассмотрены описание и использование функций в Python , рекурсия , передача и возврат функций в качестве параметров, обработка последовательностей и итераторы , а также такое понятие как генератор . Будет продемонстрировано, что в Python функции являются объектами (и, значит, могут быть переданы в качестве параметров и возвращены в результате выполнения функций). Кроме того, речь пойдет о том, как можно реализовать некоторые механизмы функционального программирования, не имеющие в Python прямой синтаксической поддержки, но широко распространенные в языках функционального программирования.

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

Функциональное программирование — это стиль программирования, использующий только композиции функций . Другими словами, это программирование в выражениях, а не в императивных командах.

Как отмечает Дэвид Мертц (David Mertz) в своей статье о функциональном программировании на Python , «функциональное программирование — программирование на функциональных языках ( LISP , ML, OCAML, Haskell, . )», основными атрибутами которых являются:

  • «Наличие функций первого класса» (функции наравне с другими объектами можно передавать внутрь функций).
  • Рекурсия является основной управляющей структурой в программе.
  • Обработка списков (последовательностей).
  • Запрещение побочных эффектов у функций, что в первую очередь означает отсутствие присваивания (в «чистых» функциональных языках)
  • Запрещение операторов, основной упор делается на выражения. Вместо операторов вся программа в идеале — одно выражение с сопутствующими определениями.
  • Ключевой вопрос: что нужно вычислить, а не как .
  • Использование функций более высоких порядков (функции над функциями над функциями).

Функциональная программа

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

Программы в функциональном стиле конструируются как композиция функций. При этом функции понимаются почти так же, как и в математике: они отображают одни объекты в другие. В программировании «чистые» функции — идеал, не всегда достижимый на практике. Практически полезные функции обычно имеют побочный эффект : сохраняют состояние между вызовами или меняют состояние других объектов. Например, без побочных эффектов невозможно представить себе функции ввода-вывода. Собственно, такие функции ради этих «эффектов» и используются. Кроме того, математические функции легко работают с объектами, требующими бесконечного объема информации (например, вещественные числа). В общем случае компьютерная

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным.

Основные концепции

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

Чистые функции

Чистая функция максимально проста. Она должна всегда возвращать один и тот же результат. Посмотрите на эту JavaScript-функцию:

var z = 10; function add(x, y)

function add (x , y ) <

Конечно, это искусственный пример, но он очень хорошо демонстрирует, как должна работать чистая функция. При одних и тех же значениях x и y мы всегда будем получать одинаковый результат функции. Предсказуемость – важная часть работы программы в функциональном программировании.

Изменяемые данные и побочные эффекты

Вернемся к примеру кода. Если мы добавим в качестве аргумента функции add() , переменную z , которая объявлена выше, наша функция перестанет быть чистой и предсказуемой. Почему? Потому что z объявлена как обычная переменная: она доступна для изменения из любого места программы.

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

Корректный код чистой функции с z должен выглядеть так:

const x = 10; const z = 10; add (x, z); // вернет 20

add (x , z ) ; // вернет 20

В этом случае функция всегда будет возвращать предсказуемый результат. Если функция не будет работать предсказуемо – это приведет к нежелательным побочным эффектам.

Еще один пример не функционального кода – классические циклы. Вспомним, как выглядит типичный цикл for в JavaScript:

var acc = 0; for (var i = 1; i end) < return acc; >else < return sumRange(start + 1, end, acc + start); >> console.log(sumRange(1, 10, 0)); // выведет 55

function sumRange (start , end , acc ) <

return sumRange (start + 1 , end , acc + start ) ;

console . log (sumRange (1 , 10 , 0 ) ) ; // выведет 55

Такая конструкция позволяет использовать константы для определения начала, конца цикла и шага. В основе такого типа цикла лежит идея вызова функции внутри себя, или рекурсивного вызова. В примере выше функция sumRange() с заданными аргументами делает проверку условия, и в случае ложного результата вызывает саму себя с измененными аргументами.

Композиция функций

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

function addOne(x) < return x + 1; >function timesTwo(x) < return x * 2; >console.log(addOne(timesTwo(3))); // выведет 7 console.log(timesTwo(addOne(3))); // выведет 8

function addOne (x ) <

function timesTwo (x ) <

В примере выше мы описали две простые функции: addOne (прибавляет к аргументу единицу) и timesTwo (умножает аргумент на два). Техника компоновки позволяет нам вызывать две эти функции друг в друге в разном порядке. В результате, с разным логическим порядком вызова чистых функций и одинаковым значением аргумента мы получили более сложный функционал, который дает нам необходимый результат и делает это предсказуемо.

Польза функционального программирования

Функциональное программирование помогает сделать код более чистым, предсказуемым и легко читаемым. Использование принципов ФП помогает избавиться от лишних абстракций с непредсказуемым поведением, следовательно, сделать работу программы более предсказуемой и снизить количество возможных ошибок.

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

Функциональное программирование плохо подходит для алгоритмов, основанных на графах из-за сравнительно более медленной работы программы. ФП в целом плохо применимо в решениях, которые годами основывались на использовании императивного подхода.

Самое важное – в функциональных языках нет эффективного неупорядоченного словаря множества. В сравнении с хэш-таблицами, функциональные словари работают значительно хуже, а это может быть критично для некоторых приложений. Однако указанные недостатки можно отнести только к императивным языкам.

Функциональное программирование в языках

Так как функциональное программирование – это прежде всего подход к написанию кода, использовать его принципы можно в любом языке. Однако существуют языки, специально заточенные под функциональный подход. Первый и самый известный из них – Lisp. Он появился еще в 1958 году. Его автор – Джон Маккарти, информатик и автор термина «искусственный интеллект». Lisp по сей день популярен в среде проектировщиков ИИ.

Более современные функциональные языки, такие как Elm и Elixir, по данным GitHub и Stack Overflow постепенно и уверенно набирают популярность. Рост популярности JavaScript также привел к повышенному интересу к концепциям функционального программирования для применения в этом языке.


Мне всегда хотелось написать серию статей по функциональному программированию для этого журнала, и я очень рад, что у меня наконец-то появилась такая возможность. Даже несмотря на то, что моя серия про анализ данных еще далека от завершения:). Не буду анонсировать содержание всей серии, скажу лишь, что сегодня мы поговорим о разных языках программирования, поддерживающих функциональный стиль и соответствующих приемах программирования.

Языки программирования, о которых не каждый знает

Я начал программировать еще в детстве, и годам к двадцати пяти мне казалось, что я все знаю и понимаю. Объектно ориентированное программирование стало частью моего мозга, все мыслимые книги о промышленном программировании были прочитаны. Но у меня оставалось такое ощущение, будто я что-то упустил, что-то очень тонкое и необыкновенно важное. Дело в том, что, как и многих в девяностые годы, в школе меня учили программировать на Pascal (о да, слава Turbo Pascal 5.5! — Прим. ред.), потом был C и C++. В университете Fortran и потом Java, как основной инструмент на работе. Я знал Python и еще несколько языков, но все это было не то. А серьезного образования в области Computer Science у меня не было. Однажды во время перелета через Атлантику я не мог заснуть, и мне захотелось что-то почитать. Каким-то волшебным образом у меня под рукой оказалась книга про язык программирования Haskell. Мне кажется, именно тогда я понял истинный смысл выражения «красота требует жертв».

Теперь, когда меня спрашивают, как я выучил Haskell, я так и говорю: в самолете. Этот эпизод изменил мое отношение к программированию вообще. Конечно, после первого знакомства многие вещи казались мне не вполне понятными. Пришлось напрячься и изучить вопрос более тщательно. И знаешь, прошло десять лет, многие функциональные элементы стали частью промышленных языков, лямбда-функции уже есть даже в Java, вывод типов — в С++, сопоставление с образцом — в Scala. Многие думают, что это какой-то прорыв. И в этой серии статей я расскажу тебе про приемы функционального программирования, используя разные языки и их особенности.

Интернетчики часто на потеху публике составляют всякие списки и топы. Например, «список книг, которые ты должен прочесть до тех пор, пока тебе не исполнилось тридцать». Если бы передо мной стояла задача сделать список книг по программированию, которые ты должен прочесть до тех пор, пока тебе сколько-то там не исполнилось, то первое место, безусловно, досталось бы книге Абельсона и Сассмана «Структура и интерпретация компьютерных программ» . Мне даже иногда кажется, что компилятор или интерпретатор любого языка должен останавливать каждого, кто не читал эту книгу.

Поэтому если и есть язык, с которого нужно начинать изучение функционального программирования, так это Lisp. Вообще, это целое семейство языков, куда входит довольно популярный сейчас язык для JVM под названием Clojure . Но в качестве первого функционального языка он не особо подходит. Для этого лучше использовать язык Scheme , который был разработан в MIT и до середины двухтысячных годов служил основным языком для обучения программированию. Хотя сейчас вводный курс с тем же названием, что упомянутая книга, был заменен на курс по Python, она все еще не потеряла своей актуальности.

Постараюсь кратко рассказать о языке Scheme и вообще об идее, стоящей за языками данной группы. Несмотря на то что Lisp очень старый (из всех языков высокого уровня старше только Fortran), именно в нем впервые стали доступны многие методы программирования, применяемые сейчас. Далее я буду использовать название Lisp, имея в виду конкретную реализацию — Scheme.

Синтаксис за две минуты

Синтаксис в языке Lisp, хм, слегка спорный. Дело в том, что идея, лежащая в основе синтаксиса, крайне проста и построена на основе так называемых S-выражений . Это префиксная запись, в которой привычное тебе выражение 2 + 3 записывается как (+ 2 3) . Это может показаться странным, но на практике дает некоторые дополнительные возможности. Кстати, (+ 2 10 (* 3.14 2)) тоже работает:). Таким образом, вся программа — это набор списков, в которых используется префиксная нотация. В случае языка Lisp сама программа и абстрактное синтаксическое дерево — «если вы понимаете, о чем я» �� — по сути, ничем не отличаются. Такая запись делает синтаксический анализ программ на Lisp очень простым.
Раз уж мы говорим о языке программирования, то следует сказать о том, как определять функции в этом языке.

Тут нужно сделать небольшое отступление. Существует одна тонкость, значимость которой в современной литературе недооценена. Нужно все-таки разделять функцию в математическом смысле и функцию, как мы ее понимаем в функциональном программировании. Дело в том, что в математике функции являются декларативными объектами, а в программировании они используются для организации процесса вычислений, то есть в каком-то смысле, скорее, представляют собой императивное знание, знание, отвечающее на вопрос «как?». Именно поэтому Абельсон и Сассман в своей книге это очень тщательно разделяют и называют функции в программировании процедурами. В современной литературе по функциональному программированию это не принято. Но я все же настоятельно рекомендую разделять эти два смысла слова «функция» хотя бы у себя в голове.

Самый простой способ определить функцию — это написать следующий код. Начнем с неприлично простого:

(define (sq-roots a b c) (let ((D (- (* b b) (* 4 a c)))) (if ( (add 4 3) => (add 5 2) => (add 6 1) => (add 7 0) => 7

Во втором случае мы будем иметь примерно следующее:

(add-1 3 4) => (succ (add-1 3 3)) => (succ (succ (add-1 3 2))) => (succ (succ (succ (add-1 3 1)))) => (succ (succ (succ (succ (add-1 3 0))))) => (succ (succ (succ (succ 3)))) => (succ (succ (succ 4))) => (succ (succ 5)) => (succ 6) => 7

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

Списки

Один из важнейших элементов функционального программирования, наряду с рекурсией, — списки . Они обеспечивают основу для сложных структур данных. Как и в других функциональных языках, списки являются односвязными по принципу голова — хвост. Для создания списка используется функция cons , а для доступа к голове и хвосту списка — функции car и cdr соответственно. Так, список (list 1 2 3) — это не что иное, как (cons 1 (cons 2 (cons 3 «()))) . Здесь «() — пустой список. Таким образом, типичная функция обработки списка выглядит так:

(define (sum lst) (if (null? lst) 0 (+ (car lst) (sum (cdr lst)))))

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

Функции высших порядков

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

(define (map f lst) (if (null? lst) lst (cons (f (car lst)) (map f (cdr lst)))))

Функция map применяет функцию f к каждому элементу списка. Как бы это странно ни выглядело, но теперь мы можем выразить функцию вычисления длины списка length через sum и map:

(define (length lst) (sum (map (lambda (x) 1) lst)))

Если ты вдруг сейчас решил, что все это как-то слишком просто, то давай подумаем вот над чем: как сделать реализацию списков, используя функции высших порядков?

То есть нужно реализовать функции cons , car и cdr так, чтобы они удовлетворяли следующему соотношению: для любого списка lst верно, что значение (cons (car lst) (cdr lst)) совпадает с lst . Это можно сделать следующим образом:

(define (cons x xs) (lambda (pick) (if (eq? pick 1) x xs))) (define (car f) (f 1)) (define (cdr f) (f 2))

Как это работает? Здесь функция cons возвращает другую функцию, которая имеет один параметр и в зависимости от этого возвращает либо первый, либо второй аргументы. Легко проверить, что необходимое соотношение выполняется для этих функций.

Использование quote и метапрограммирование

Одна приятная особенность языка Lisp делает его необыкновенно удобным для написания программ, которые занимаются преобразованием других программ. Дело в том, что программа состоит из списков, а список — это основная структура данных в языке. Существует способ просто «закавычить» текст программы, чтобы она воспринималась как список атомов.

Атомы — это просто символьные выражения, к примеру («hello «world) , что то же самое, что и «(hello world) , или в полной форме (quote (hello world)) . Несмотря на то что в большинстве диалектов Lisp есть строки, иногда можно обходиться quote . Что более важно, с помощью такого подхода можно упростить кодогенерацию и обработку программ.

Для начала попробуем разобраться с символьными вычислениями. Обычно под этим понимают системы компьютерной алгебры, которые способны обращаться с символьными объектами, с формулами, уравнениями и прочими сложными математическими объектами (таких систем много, основными примерами служат системы Maple и Mathematica ).

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

Так что я лишь приведу пример кода, который бы показывал суть дела, детали оставлю читателю (который, как я надеюсь, тщательно изучит книгу «Структура и интерпретация компьютерных программ»).

(define (deriv exp var) (cond ((number? exp) 0) ((variable? exp) (if (same-variable? exp var) 1 0)) ((sum? exp) (make-sum (deriv (addend exp) var) (deriv (augend exp) var))) ((product? exp) (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)) (make-product (deriv (multiplier exp) var) (multiplicand exp)))) (else (error «unknown expression type — DERIV» exp))))

Здесь функция deriv представляет собой реализацию алгоритма дифференцирования так, как его проходят в школе. Данная функция требует реализации функций number? , variable? и так далее, которые позволяют понять, какую природу имеет тот или иной элемент выражения. Также нужно реализовать дополнительные функции make-product и make-sum . Здесь используется пока неизвестная нам конструкция cond — это аналог оператора switch в таких языках программирования, как C и Java.

Перед тем как мы перейдем к реализации недостающих функций, стоит отметить, что в функциональном программировании довольно часто используется top-down подход к разработке. Это когда сначала пишутся самые общие функции, а затем небольшие функции, отвечающие за детали реализации.

(define (variable? x) (symbol? x)) (define (same-variable? v1 v2) (and (variable? v1) (variable? v2) (eq? v1 v2))) (define (make-sum a1 a2) (list «+ a1 a2)) (define (make-product m1 m2) (list «* m1 m2)) (define (sum? x) (and (pair? x) (eq? (car x) «+))) (define (addend s) (cadr s)) (define (augend s) (caddr s)) (define (product? x) (and (pair? x) (eq? (car x) «*))) (define (multiplier p) (cadr p)) (define (multiplicand p) (caddr p))

Реализация данных функций не требует специальных комментариев, за исключением, может быть, функций cadr и caddr . Это не что иное, как функции, которые возвращают второй и третий элементы списка соответственно.

Если воспользоваться интерактивным интерпретатором Scheme, то легко убедиться, что полученный код работает правильно, но без упрощения выражений:

(deriv «(+ x 3) «x) => (+ 1 0) (deriv «(* (* x y) (+ x 3)) «x) => (+ (* (* x y) (+ 1 0)) (* (+ (* x 0) (* 1 y)) (+ x 3)))

Для тривиальных случаев (например, умножение на 0) задача упрощения решается довольно легко. Этот вопрос остается читателю. Большинство примеров в этой статье взяты из книги SICP, поэтому в случае возникновения трудностей можно просто обратиться к источнику (книга находится в открытом доступе).

Как и любой диалект, Lisp имеет большие возможности в метапрограммировании, по большей части связанные с использованием макросов. К сожалению, этот вопрос требует разбора в отдельной статье.

Давай напишем функцию, которая будет удалять синтаксический сахар из определения функции так, как это обсуждалось ранее:

(define (desugar-define def) (let ((fn-args (cadr def)) (body (caddr def))) (let ((name (car fn-args)) (args (cdr fn-args))) (list «define name (list «lambda args body)))))

Эта функция прекрасно работает с правильно сформированными определениями функций:

(desugar-define «(define (succ x) (+ x 1))) => (define succ (lambda (x) (+ x 1)))

Однако это не работает для обычных определений, таких как (define x 5) .
Если мы хотим удалить синтаксический сахар в большой программе, содержащей множество различных определений, то мы должны реализовать дополнительную проверку:

(define (sugared? def) (and (eq? (car def) «define) (list? (cadr def))))

Такую проверку можно встроить прямо в функцию desugar-define , сделав так, чтобы в случае, если определение не нуждается в удалении синтаксического сахара, оно просто бы не менялось (данное тривиальное упражнение остается читателю). После чего можно обернуть всю программу в список и использовать map:

(map desugar-define prog)

Заключение

В данной статье я не ставил себе задачу рассказать про Scheme сколь-нибудь подробно. Мне прежде всего хотелось показать несколько интересных особенностей языка и привлечь читателя к изучению функционального программирования. Этот чудесный язык при всей его простоте имеет свое очарование и особенности, которые делают программирование на нем очень увлекательным. Что касается инструмента для работы со Scheme, то сильные духом могут замахнуться на MIT-Scheme , а остальные — пользуйтесь прекрасной учебной средой Dr. Racket . В одной из следующих статей я обязательно расскажу, как написать собственный интерпретатор Scheme.

Итак, вы хотите научиться функциональному программированию (Часть 1)

Первый шаг к пониманию идей функционального программирования — самый важный и иногда самый сложный шаг. Но с правильным подходом никаких трудностей быть не должно.

Обучение вождению

Когда мы только учились водить машину, мы старались изо всех сил. Конечно, это выглядело легко, когда мы смотрели, как водят другие люди. Но на деле всё оказывалось сложнее.

Мы упражнялись на машине родителей и не выезжали на автострады, пока в совершенстве не осваивали улицы родного района.

После множества практических занятий и нескольких щекотливых моментов, о которых наши родители хотели бы забыть, мы выучивались вождению и наконец получали свои водительские права.

С правами в руках мы садились за руль при любом удобном случае. С каждой новой поездкой наши навыки становились всё лучше и лучше, уверенность росла. Затем наступал день, когда нам приходилось вести другую машину или наша собственная приказывала долго жить, и мы покупали новую.

Вспомните, на что был похож первый раз за рулём другой машины? Было ли это похоже на самый первый раз за рулём машины вообще? Даже не близко. В первый раз всё было таким незнакомым. Конечно, мы сидели до этого в машине, но только в роли пассажира. На этот раз мы оказывались в сидении водителя. Один на один со всеми рычагами, кнопками и педалями.

Но когда мы вели нашу вторую машину, мы просто задавали себе несколько простых вопросов по типу: куда вставляется ключ, где переключаются фары ближнего и дальнего света, как использовать поворотники и как настроить зеркала заднего вида.

После всего этого мы вели свою машину как по маслу. Но почему в этот раз всё было так просто по сравнению с первым разом?

Потому что новая машина была достаточно похожа на старую. Она имела все те базовые элементы, что нужны машине, и в большинстве случаев они находились на тех же местах, что и в старой.

Может, несколько вещей были реализованы как-то иначе и, может быть, они имели какие-то дополнительные функциональные возможности, но мы и так не использовали их во всём нашем водительском опыте. Рано или поздно мы изучали всё новые примочки. Как минимум, те, что нам реально требовались.

Что ж, процесс изучения языков программирования похож на процесс обучения вождению. Первый раз — самый сложный. Но с багажом опыта за плечами всё последующее обучение становится проще.

Когда вы начинаете изучать второй язык, вы спрашиваете себя: “Как мне создать модуль? Как реализовать поиск по массиву? Какие параметры принимает функция нахождения подстроки?”.

Вы уверены, что можете «научиться водить» на этом новом языке, потому что он напоминает вам о предыдущем языке, может, с несколькими новыми элементами, которые, надо надеяться, сделают вашу жизнь легче.

Ваш первый космический корабль

Ездили ли вы на одном автомобиле всю свою жизнь или на десятках автомобилях, представьте, что собираетесь сесть за штурвал космического корабля.

Если вы собираетесь летать на таком аппарате, то вряд ли будете надеяться, что навыки вождения на дороге как-то особенно вам помогут. Вы начнёте всё с нуля ( Мы же программисты, чёрт возьми. Мы начинаем считать с нуля.).

Вы начнёте свои тренировки с расчётом, что в космосе всё работает иначе и что полёты на этой штуковине довольно отличны от вождения машины по земле.

Однако физика не изменилась. Путь, по которому вы двигаетесь, находится в пределах всё той же Вселенной.

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

Программировать — значит мыслить и функциональное программирование научит вас мыслить совсем по-другому настолько, что вы вероятно никогда не вернётесь назад на старый путь образа мышления.

Забудьте всё, что вы знаете

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

С правильным подходом у вас будут правильные ожидания, а с правильными ожиданиями вы не захотите бросить дело, когда начнутся вещи по-тяжелее.

Есть также многие вещи, которые вы привыкли делать, как программист, но которые вы не сможете больше делать, занимаясь функциональным программированием.

Вспомните, как вы, чтобы выехать с проезжей части, давали задний ход на машине. Но на космическом корабле нет механизма реверса. Теперь вы должны подумать: “ЧТО? НЕТ ЗАДНЕГО ХОДА? КАК Я ДОЛЖЕН ВОДИТЬ БЕЗ ЗАДНЕГО ХОДА?”.

Что ж, оказывается вам не нужен реверс на космическом корабле, способном маневрировать в трёхмерном пространстве космоса. Как только вы поймёте это, вы больше не будете вспоминать о возможности заднего хода. И однажды, вы даже задумаетесь о том, насколько, на самом деле, ограничены обыкновенные машины.

Изучение функционального программирования требует времени. Запаситесь терпением.

Так что давайте покинем холодный мир императивного и медленно окунёмся в горячие источники функционального программирования.

То, что следует в этой комплексной статье, — концепции функционального программирования, которые помогут вам перед полным погружением в первый функциональный язык. Или, если вы уже сделали решительный шаг в этой сфере, эти параграфы помогут заточить понимание идей.

Пожалуйста, не спешите. С этого момента не торопитесь и находите время, чтобы понять примеры кода. Лучше будет даже сделать небольшую паузу в прочтении после этой части статьи и дать озвученным идеям “устаканиться”. Затем возвращайтесь к чтению.

Самое главное — это то, чтобы вы поняли.

Чистота

Если говорится о чистоте в функциональном программировании, значит подразумеваются чистые функции.

Чистые функции — очень простые. Они всего лишь производят операция над входными данными.

Вот пример чистой функции:

Заметьте, что функция add не прикасается к переменной z . Она не читает её значения и ничего не пишет в неё. Функция читает только x и y , свои входные данные, и возвращает результат их суммы.

Это и есть чистая функция. Если функция add имеет доступ к переменной z , она больше не может быть чистой.

Это пример другой чистой функции:

Если функция justTen чистая, она может возвращать только значение-константу. Почему?

Потому что мы не даём ей никаких входных данных. А значит, чтобы быть чистой, она не должна изменять никаких переменных, кроме тех, что были ей переданы. Единственное, что может возвратить такая функция — константа.

Пока функции, не принимающие параметров, не работают, они не очень полезны. Было бы лучше объявить justTen просто как константу.

Более полезные чистые функции принимают хотя бы один параметр.

Взгляните на этот пример:

Посмотрите, эта функция ничего не возвращает. Она складывает x и y , записывает результат в переменную z , но не возвращает её.

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

Все полезные чистые функции должны возвращать что-нибудь.

Давайте рассмотрим пример с первой функцией add ещё раз:

Обратите внимание, что add(1, 2) в результате всегда даёт 3 . Конечно, сюрприз не большой, но это потому что функция чистая. Если бы функция add брала значение откуда-то снаружи, вы бы никогда не могли наверняка предсказать её поведение.

Чистая функция всегда возвращает одинаковые значения для одинаковых входных данных .

Поскольку чистые функции не могут изменять внешние переменные, все эти функции являются нечистыми:

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

Чистые функции не имеют побочных эффектов.

В таких языках императивного программирования как JavaScript, Java и C# побочные эффекты везде. Это делает отладку проблематичной, потому что в коде Вашей программы переменная может быть изменена где угодно. В общем, если у Вас баг из-за переменой, принявшей неверное значение в неподходящее время, где Вы будете искать ошибку? Везде? Так дело не пойдёт.

Мастер Йода рекомендует:  Супершпаргалка по верстке для новичков все основные HTML-теги

На этом месте, вы, вероятно, думаете: “КАК, ЧЁРТ ПОБЕРИ, Я СДЕЛАЮ ХОТЬ ЧТО-НИБУДЬ ОДНИМИ ТОЛЬКО ЧИСТЫМИ ФУНКЦИЯМИ?”.

В функциональном программировании вы не пишите только чистые функции.

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

Неизменяемость

Вы помните, когда впервые увидели следующий код:

И тот, кто учил вас программированию, говорил забыть изученное на уроках математики. Ведь в математике 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 при игре.

Неизменяемость делает код проще и безопаснее.

Мой мозг.

Пока что достаточно.

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

Языки функционального программирования. Принципы функционального программирования: почему это важно

Программы на традиционных языках программирования, таких как Си, Паскаль, Java и т.п. состоят их последовательности модификаций значений некоторого набора переменных, который называется состоянием . Если не рассматривать операции ввода-вывода, а также не учитывать того факта, что программа может работать непрерывно (т.е. без остановок, как в случае серверных программ), можно сделать следующую абстракцию. До начала выполнения программы состояние имеет некоторое начальное значение σ0 , в котором представлены входные значения программы. После завершения программы состояние имеет новое значение σ0 , включающее в себя то, что можно рассматривать как «результат» работы программы. Во время исполнения каждая команда изменяет состояние; следовательно, состояние проходит через некоторую конечную последовательность значений:

σ = σ0 → σ1 → σ2 → · · · → σn = σ0

Состояние модифицируется с помощью команд присваивания , записываемых в виде v=E или v:=E, где v — переменная, а E — некоторое выражение. Эти команды следуют одна за другой; операторы, такие как if и while, позволяют изменить порядок выполнения этих команд в зависимости от текущего значения состояния. Такой стиль программирования называютимперативным илипроцедурным .

Функциональное программирование представляет парадигму, в корне отличную от представленной выше модели. Функциональная программа представляет собой некоторое выражение (в математическом смысле); выполнение программы означает вычисление значения этого выражения.1 С учетом приведенных выше обозначений, считая что результат работы

1 Употребление термина «вычисление» не означает, что программа может оперировать только с числами; результатом вычисления могут оказаться строки, списки и вообще, любые допустимые в языке структуры данных.

императивной программы полностью и однозначно определен ее входом, можно сказать, что финальное состояние (или любое промежуточное) представляет собой некоторую функцию (в математическом смысле) от начального состояния, т.е. σ0 = f(σ). В функционально программировании используется именно эта точка зрения: программа представляет собой выражение, соответствующее функции f. Функциональные языки программирования поддерживают построение таких выражений, предоставляю широкий выбор соответствующих языковых конструкций.

При сравнении функционального и императивного подхода к программированию можно заметить следующие свойства функциональных программ:

Функциональные программы не используют переменные в том смысле, в котором это слово употребляется в императивном программировании. В частности в функциональных программах не используется оператор присваивания.

Как следствие из предыдущего пункта, в функциональных программах нет циклов.

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

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

Вместо циклов функциональные программы широко используют рекурсивные функции.

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

Прежде всего, императивный стиль в программировании не является жестко заданной необходимостью. Многие характеристики императивных языков программирования являются результатом абстрагирования от низкоуровневых деталей реализации компьютера, от машинных кодов к языкам ассемблера, а затем к языкам типа Фортрана и т.д. Однако нет причин полагать, что такие языки отражают наиболее естественный для

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

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

Например, рассмотрим следующую программу на языке Haskell:

factorial n = if n == 0 then 1 else n * factorial (n — 1)

Практически сразу видно, что эта программа соответствует следующей частичной функции:

(Здесь символ означает неопределенность функции, поскольку при отрицательных значениях аргумента программа не завершается.) Однако для программы на языке Си это соответствие не очевидно:

int x = 1; while (n > 0)

x = x * n; n = n — 1;

Следует также сделать замечание относительно употребления термина «функция» в таких языках как Си, Java и т.п. В математическом смысле «функции» языка Си не являются функциями, поскольку:

Их значение может зависеть не только от аргументов;

Результатом их выполнения могут быть разнообразные побочные эффекты (например, изменение значений глобальных переменных)

Два вызова одной и той же функции с одними и теми же аргументами могут привести к различным результатам.

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

2 Основы лямбда-исчисления

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

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

В настоящее время лямбда-исчисление является основной из таких формализаций, применяемой в исследованиях связанных с языками программирования. Связано это, вероятно, со следующими факторами:

Это единственная формализация, которая, хотя и с некоторыми неудобствами, действительно может быть непосредственно использована для написания программ.

Лямбда-исчисление дает простую и естественную модель для таких важных понятий, как рекурсия и вложенные среды.

Большинство конструкций традиционных языков программирования может быть более или менее непосредственно отображено в конструкции лямбда-исчисления.

Функциональные языки являются в основном удобной формой синтаксической записи для конструкций различных вариантов лямбдаисчисления. Некоторые современные языки (Haskell, Clean) имеют

100% соответствие своей семантики с семантикой подразумеваемых конструкций лямбда-исчисления.

В математике, когда необходимо говорить о какой-либо функции, принято давать этой функции некоторое имя и впоследствии использовать его, как, например, в следующем утверждении:

Пусть f: R → R определяется следующим выражением:

Тогда f0 (x) не интегрируема на интервале .

Многие языки программирования также допускают определение функций только с присваиванием им некоторых имен. Например, в языке Си функция всегда должна иметь имя. Это кажется естественным, однако поскольку в функциональном программировании функции используются повсеместно, такой подход может привести к серьезным затруднениям. Представьте себе, что мы должны всегда оперировать с арифметическими выражениями в подобном стиле:

Пусть x = 2 и y = 4. Тогда xx = y.

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

где E — некоторое выражение, возможно, использующее переменную x.

Пример. λx.x2 представляет собой функцию, возводящую свой аргумент в квадрат.

Использование лямбда-нотации позволяет четко разделить случаи, когда под выражением вида f(x) мы понимаем саму функцию f и ее значение в точке x. Кроме того, лямбда-нотация позволяет формализовать практически все виды математической нотации. Если начать с констант и переменных и строить выражения только с помощью лямбда-выраже- ний и применений функции к аргументам, то можно представить очень сложные математические выражения.

Применение функции f к аргументу x мы будем обозначать как f x, т.е., в отличие от того, как это принято в математике, не будем использовать скобки2 . По причинам, которые станут ясны позднее, будем считать, что применение функции к аргументу ассоциативно влево, т.е. f x y

2 Заметим, что и в математике такие выражения, как sin x записываются без скобок.

означает (f(x))(y). В качестве сокращения для выражений вида λx.λy.E будем использовать запись λx y.E (аналогично для большего числа аргументов). Также будем считать, что «область действия» лямбда-выра- жения простирается вправо насколько возможно, т.е., например, λx.x y означает λx.(x y), а не (λx.x)y.

На первый взгляд кажется, что нам необходимо ввести специальное обозначение для функций нескольких аргументов. Однако существует операция каррирования 3 , позволяющая записать такие функции в обычной лямбда-нотации. Идея заключается в том, чтобы использовать выражения вида λx y.x + y. Такое выражение можно рассматривать как функцию R → (R → R), т.е. если его применить к одному аргументу, результатом будет функция, которая затем принимает другой аргумент. Таким образом:

(λx y.x + y) 1 2 = (λy.1 + y) 2 = 1 + 2.

Переменные в лямбда-выражениях могут бытьсвободными исвязанными . В выражении вида x2 + x переменная x является свободной; его значение зависит от значения переменной x и в общем случае ее нельзя

вать обозначение j, значение выражения не изменится.

Следует понимать, что в каком-либо подвыражении переменная может быть свободной (как в выражении под интегралом), однако во всем выражении она связана какой-либо операцией связывания переменной , такой как операция суммирования. Та часть выражения, которая находится «внутри» операции связывания, называется областью видимости переменной.

В лямбда исчислении выражения λx.E[x] и λy.E[y] считаются эквивалентными (это называется α-эквивалентностью, и процесс преобразования между такими парами называют α-преобразованием). Разумеется, необходимо наложить условие, что y не является свободной переменной в E[x].

3 от фамилии известного логика Хаскелла Карри, в честь которого назван язык программирования Haskell

3 Лямбда-исчисление как формальная система

Лямбда-исчисление основано на формальной нотации лямбда-терма, составляемого из переменных и некоторого фиксированного набора констант с использованием операции применения функции и лямбда-абстра- гирования. Сказанное означает, что все лямбда-выражения можно разделить на четыре категории:

1. Переменные: обозначаются произвольными строками, составленными из букв и цифр.

2. Константы: также обозначаются строками; отличие от переменных будем определять из контекста.

3. Комбинации: , т.е. применения функции S к аргументу T ; и S и T могут быть произвольными лямбда-термами. Комбинация записывается как S T .

4. Абстракции произвольного лямбда-терма S по переменной x, обозначаемые как λx.S.

Таким образом, лямбда-терм определяется рекурсивно и его грамматику можно определить в виде следующей формы Бэкуса-Наура:

Exp = Var| Const| Exp Exp| λ Var . Exp

В соответствие с этой грамматикой лямбда-термы представляются в виде синтаксических деревьев, а не в виде последовательности символов. Отсюда следует, что соглашения об ассоциативности операции применения функции, эквивалентность выражений вида λx y.S и λx.λy.S, неоднозначность в именах констант и переменных проистекают только из необходимости представления лямбда-термов в удобном человеку виде, и не являются частью формальной системы.

3.1 Свободные и связанные переменные

В данном разделе мы формализуем данное ранее интуитивное представление о свободных и связанных переменных. Множество свободных

переменных F V (S) лямбда-терма S можно определить рекурсивно следующим образом:

Аналогично множество связанных переменных BV (S) определяется следующими формулами:

BV (S T) = BV (S) BV (T)

Здесь предполагается, что c — некоторая константа.

Пример. Для терма S = (λx y.x) (λx.z x) можно показать, что F V (S) = и

Интуитивно ясно, что применение терма λx.S как функции к аргументу T дает в результате терм S, в котором все свободные вхождения переменной x заменены на T . Как ни странно, формализовать это интуитивное представление оказывается нелегко.

Будем обозначать операцию подстановки терма S вместо переменной x в другом терме T как T . Также, как и в определение свободных и связанных переменных, правила подстановки также можно определить рекурсивно. Трудность состоит в том, что необходимо наложить дополнительные ограничения, позволяющие избегать конфликта в именах переменных.

Лямбда-исчисление основано на трех операциях конверсии, которые позволяют переходить от одного терма к другому, эквивалентному ему. По сложившейся традиции эти конверсии обозначают греческими буквами α, β и η. Они определяются следующим образом:

α-конверсия: λx.S −→ λy.S при условии, что y / F V (S).

Например, λu.u v −→ λw.w u.

β-конверсия: (λx.S) T −→ S.

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

3.4 Равенство лямбда-термов

Используя введенные правила конверсии, можно формально определить понятие равенства лямбда-термов. Два терма равны, если от одного из них можно перейти к другому с помощью конечной последовательности конверсий. Определим понятие равенства следующими выражениями, в которых горизонтальные линии следует понимать как «если утверждение над чертой выполняется, то выполняется и утверждение

Следует отличать понятие равенства, определяемое этими формулами, от понятия синтаксической эквивалентности, которую мы будем обозначать специальным символом ≡. Например, λx.x 6≡λy.y, но λx.x = λy.y. Часто можно рассматривать синтаксическую эквивалентность термов с точностью до α-конверсий. Такую эквивалентность будем обозначать символом ≡α . Это отношение определяется так же, как равенство лямбда-термов, за тем исключением, что из всех конверсий допустимы только α-конверсии. Таким образом, λx.x ≡α λy.y.

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

Подобные языки к функциональным, использующим менее строгое понятие. Функция в математике не может изменить вызывающее её окружение и запомнить результаты своей работы, а только предоставляет результат вычисления функции. Программирование с использованием математического понятия функции вызывает некоторые трудности, поэтому функциональные языки, в той или иной степени предоставляют и императивные возможности, что ухудшает дизайн программы (например возможность безболезненных дальнейших изменений). Дополнительное отличие от императивных языков программирования заключается в декларативности описаний функций. Тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. Первым, спроектированным функциональным языком стал Лисп . Вариант данного языка широко используется в системе автоматизированного проектирования AutoLISP

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

  • краткость и простота;
  • строгая типизация;
  • модульность;
  • функции — объекты вычисления;
  • отложенные (ленивые) вычисления.

Некоторые языки функционального программирования

Ссылки

  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года.

Wikimedia Foundation . 2010 .

Смотреть что такое «Функциональный язык программирования» в других словарях:

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

функциональный язык — Язык программирования, в котором действия над данными выражаются в виде обращений к функциональным процедурам. [ГОСТ 19781 90] Тематики обеспеч. систем обраб. информ. программное EN functional language … Справочник технического переводчика

Ruby Семантика: мультипарадигмальный Тип исполнения: интерпретатор Появился в: 1995 г. Автор(ы): Юкихиро Мацумото Последняя версия: 1.9.1 … Википедия

Функциональный язык — 37. Функциональный язык Functional language Язык программирования, в котором действия над данными выражаются в виде обращений к функциональным процедурам Источник: ГОСТ 19781 90: Обеспечение систем обработки информации программное. Термины и… … Словарь-справочник терминов нормативно-технической документации

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

Scheme Семантика: функциональный Тип исполнения: интерпретатор или компилятор Появился в: 1970 г. Автор(ы): Гай Стил и Джеральд Сассмен Типизация данных … Википедия

У этого термина существуют и другие значения, см. Миранда. Miranda функциональный язык программирования, созданный в 1985 году Дэвидом Тёрнером в качестве стандартного функционального языка. Имеет строгую полиморфную систему типов,… … Википедия

Hope функциональный язык программирования, разработанный в начале 1980 х годов; является предшественником языков Miranda и Haskell. В журнале Byte за август 1985 впервые опубликовано руководство по языку Hope. Пример программы вычисления… … Википедия

У этого термина существуют и другие значения, см. SASL. SASL полностью функциональный язык программирования, разработанный Дэвидом Тёрнером в Сент Эндрюсском университете в 1972 году, на базе аппликативного подмножества ISWIM. В 1976 году… … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Книги

  • Программирование в Clojure. Практика применения Lisp в мире Java , Эмерик Ч., Карпер Б., Гранд К.. Почему многие выбирают Clojure? Это — функциональный язык программирования, не только позволяющий пользоваться Java-библиотеками, службами и другими ресурсами JVM, но и соперничающий с…

String reverse(String arg) < if(arg.length == 0) < return arg; >else < return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); >>
Эта функция довольно медленная, потому что она повторно вызывает сама себя . Здесь возможна утечка памяти, так как множество раз создаются временные объекты. Но это функциональный стиль. Вам может показать странным, как люди могут так программировать. Ну, я как раз собирался вам рассказать.

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

Unit тестирование

Вот она, голубая мечта unit-тестеров. Можно протестировать каждую функцию в программе используя только нужные аргументы. Нет необходимости вызывать функции в правильном порядке или воссоздавать правильное внешнее состояние. Всё что вам нужно, это передать аргументы, которые соответствуют граничным случаям. Если все функции в вашей программе проходят Unit-тесты, то вы можете быть намного более уверены в качестве вашего ПО, чем в случае императивных языков программирования. В Java или C++ проверки возвращаемого значения не достаточно — функция может поменять внешнее состояние, которое тоже подлежит проверке. В ФП такой проблемы нет.

Отладка

Как только вы воспроизведёте ошибку, найти её источник — тривиальная задача. Это даже приятно. Как только вы остановите выполнение программы, перед вами будет весь стек вызовов. Вы можете просмотреть аргументы вызова каждой функции, прямо как в императивном языке. С тем отличием, что в императивной программе этого не достаточно, ведь функции зависят от значений полей, глобальных переменных и состояний других классов. Функция в ФП зависит только от своих аргументов, и эта информация оказывается прямо у вас перед глазами! Даже больше, в императивной программе проверки возвращаемого значения не достаточно для того, чтобы сказать, правильно ли ведёт себя кусок кода. Вам придётся выследить десятки объектов за пределами функции, чтобы удостовериться, что всё работает правильно. В функциональном программировании всё, что нужно сделать — это взглянуть на возвращаемое значение!

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

Многопоточность

Если дела обстоят подобным образом, то почему так редко функциональные языки программирования используются в многопоточных приложениях? На самом деле чаще, чем вы думаете. Компания Ericsson разработала функциональный язык под названием Erlang для использования на отказоустойчивых и масштабируемых телекоммуникационных коммутаторах. Многие отметили преимущества Erlang-а и стали его использовать . Мы говорим о телекоммуникациях и системах контроля трафика, которые далеко не так просто масштабируются, как типичные системы, разработанные на Wall Street. Вообще-то, системы написанные на Erlang, не такие масштабируемые и надёжные, как Java системы. Erlang системы просто сверхнадёжные.

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

Компилятор функционального языка может проанализировать код, классифицировать функции, которые создают строки s1 и s2 , как функции потребляющие много времени, и запустить их параллельно. Это невозможно сделать в императивном языке, потому что каждая функция может изменять внешнее состояние и код, идущий непосредственно после вызова, может зависеть от неё. В ФП автоматический анализ функций и поиск подходящих кандидатов для распараллеливания — это тривиальнейшая задача, как автоматический inline ! В этом смысле функциональный стиль программирования соответствует требованиям завтрашнего дня. Разработчики железа уже не могут заставить CPU работать быстрее. Вместо этого они наращивают количество ядер и заявляют о четырёхкратном увеличении скорости многопоточных вычислений. Конечно они очень вовремя забывают сказать, что ваш новый процессор покажет прирост только в программах, разработанных с учётом распараллеливания. Среди императивного ПО таких очень мало. Зато 100% функциональных программ готовы к многопоточности из коробки.

Развёртывание по горячему

В идеале нужно обновить все нужные участки кода не останавливая систему в принципе. В императивном мире это невозможно [пер. в Smalltalk-е очень даже возможно]. Представьте себе выгрузку Java класса на лету и перезагрузка новой версии. Если бы мы так сделали, то все экземпляры класса стали бы нерабочими, потому что потерялось бы состояние, которое они хранили. Нам пришлось бы писать хитрый код, для контроля версий. Пришлось бы серриализовать все созданные экземпляры класса, потом уничтожить их, создать экземпляры нового класса, попытаться загрузить серриализованные данные в надежде, что миграция пройдёт нормально и новые экземпляры будут валидными. И кроме того, миграционный код необходимо писать каждый раз вручную. И ещё миграционный код должен сохранять ссылки между объектами. В теории ещё куда ни шло, но на практике это никогда не заработает.

В функциональной программе всё состояние хранится в стеке в виде аргументов функций. Это позволяет значительно упростить развёртывание по горячему! По сути всё что нужно сделать — это вычислить разницу между кодом на рабочем сервере и новой версией, и установить изменения в коде. Остальное будет сделано языковыми инструментами автоматически! Если вы думаете, что это научная фантастика, то дважды подумайте. Инженеры, имеющие дело с Erlang, годами обновляют свои системы без остановки их работы.

Доказательные вычисления и оптимизация (Machine Assisted Proofs and Optimizations)

Дополнительно вы можете использовать математический аппарат, чтобы доказать корректность участков ваших программ. При желании можно написать инструменты, которые анализируют код и автоматически создают Unit-тесты для граничных случаев! Такая функциональность бесценна для сверхнадёжных систем (rock solid systems). При разработке систем контроля кардиостимуляторов или управления воздушным трафиком такие инструменты просто необходимы. Если же ваши разработки не находятся в сфере критически важных приложений, то инструменты автоматической проверки всё равно дадут вам гигантское преимущество перед вашими конкурентами.

Функции высшего порядка

В ФП функция — это не тоже самое, что функция в Java или C. Это надмножество — они могут тоже самое, что Java функции и даже больше. Пусть у нас есть функция на C:

Int add(int i, int j) < return i + j; >
В ФП это не тоже самое, что обычная C функция. Давайте расширим наш Java компилятор, чтобы он поддерживал такую запись. Компилятор должен превратить объявление функции в следующий Java код (не забывайте, что везде присутствует неявный final):

> Символ add не совсем функция. Это маленький класс с одним методом. Теперь мы можем передавать add в качестве аргумента в другие функции. Мы можем записать его в другой символ. Мы можем создавать экземпляры add_function_t в runtime и они будут уничтожены сборщиком мусора, если станут ненужными. Функции становятся базовыми объектами, как числа и строки. Функции, которые оперируют функциями (принимают их в качестве аргументов) называются функциями высшего порядка. Пусть это вас не пугает. Понятие функций высшего порядка почти не отличается от понятия Java классов, которые оперируют друг другом (мы можем передавать классы в другие классы). Мы можем называть их «классы высшего порядка», но никто этим не заморачивается, потому что за Java не стоит строгое академическое сообщество.

Как и когда нужно использовать функции высшего порядка? Я рад, что вы спросили. Вы пишите свою программу как один большой монолитный кусок кода не заботясь об иерархии классов. Если вы увидите, что какой-то участок кода повторяется в разных места, вы выносите его в отдельную функцию (к счастью в школах еще учат как это делать). Если вы замечаете, что часть логики в вашей функции должна вести себя по разному в некоторых ситуациях, то вы создаёте функцию высшего порядка. Запутались? Вот реальный пример из мой работы.

Предположим, что у нас есть участок Java кода, который получает сообщение, преобразует его различными способами и передаёт на другой сервер.

Void handleMessage(Message msg) < // . msg.setClientCode("ABCD_123"); // . sendMessage(msg); >// . >
Теперь представьте себе, что система поменялась, и теперь нужно распределять сообщения между двумя серверами вместо одного. Всё остаётся неизменным, кроме кода клиента — второй сервер хочет получать этот код в другом формате. Как нам справиться с этой ситуацией? Мы можем проверять, куда должно попасть сообщение, и в зависимости от этого устанавливать правильный код клиента. Например так:

Class MessageHandler < void handleMessage(Message msg) < // . if(msg.getDestination().equals("server1") < msg.setClientCode("ABCD_123"); >else < msg.setClientCode("123_ABC"); >// . sendMessage(msg); > // . >
Но такой подход плохо масштабируется. При добавлении новых серверов функция будет расти линейно, и внесение изменений превратится в кошмар. Объектно ориентированный подход заключается в выделении общего суперкласса MessageHandler и вынесение логики определения кода клиента в подклассы:

Abstract class MessageHandler < void handleMessage(Message msg) < // . msg.setClientCode(getClientCode()); // . sendMessage(msg); >abstract String getClientCode(); // . > class MessageHandlerOne extends MessageHandler < String getClientCode() < return "ABCD_123"; >> class MessageHandlerTwo extends MessageHandler < String getClientCode() < return "123_ABCD"; >>
Теперь для каждого сервера мы можем создать экземпляр соответствующего класса. Добавление новых сервером становится более удобным. Но для такого небольшого изменения многовато текста. Пришлось создать два новых типа чтобы просто добавить поддержку различного кода клиента! Теперь сделаем тоже самое в нашем языке с поддержкой функций высшего порядка:

; > MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
Мы не создавали новых типов и не усложняли иерархию классов. Мы просто передали функцию в качестве параметра. Мы достигли того же эффекта, как и в объектно-ориентированном аналоге, только с некоторыми преимуществами. Мы не привязывали себя к какой-либо иерархии классов: мы можем передавать любые другие функции в runtime и менять их в любой момент, сохраняя при этом высокий уровень модульности меньшим количеством кода. По сути компилятор создал объектно-ориентированный «клей» вместо нас! При этом сохраняются все остальные преимущества ФП. Конечно абстракции, предлагаемые функциональными языками на этом не заканчиваются. Функции высшего порядка это только начало

Каррирование

Функциональные языки невероятно выразительны. В функциональном языке вам не понадобятся паттерны проектирования, потому что язык настолько высокоуровневый, что вы легко начнёте программировать в концепциях, которые исключают все известные паттерны программирования. Одним из таких паттернов является Адаптер (чем он отличается от Фасада? Похоже, что кому-то понадобилось наштамповать побольше страниц, чтобы выполнить условия контракта). Этот паттерн оказывается ненужным если в языке есть поддержка каррирования.

Паттерн Адаптер наиболее часто применяется к «стандартной» единице абстракции в Java — классу. В функциональных языках паттерн применяется к функциям. Паттерн берёт интерфейс и преобразует его в другой интерфейс, согласно определённым требованиям. Вот пример паттерна Адаптер:

Int pow(int i, int j); int square(int i) < return pow(i, 2); >
Этот код адаптирует интерфейс функции, возводящей число в произвольную степень, к интерфейсу функции, которая возводит число в квадрат. В аккадемических кругах этот простейший приём называется каррирование (в честь специалиста по логике Хаскелла Карри (Haskell Curry), который провёл ряд математических трюков, чтобы всё это формализовать). Так как в ФП функции используются повсеместно в качестве аргументов, каррирование используется очень часто, чтобы привести функции к интерфейсу, необходимому в том или ином месте. Так как интерфейс функции — это её аргументы, то каррирование используется для уменьшения количества аргументов (как в примере выше).

Этот инструмент является встроенным в функциональные языки. Вам не нужно вручную создавать функцию, которая оборачивает оригинал. Функциональный язык сделает всё за вас. Как обычно давайте расширим наш язык, добавив в него каррирование.

Square = int pow(int i, 2);
Этой строкой мы автоматически создаём функцию возведения в квадрат с одним аргументом. Новая функция будет вызывать функцию pow , подставляя 2 в качестве второго аргумента. С точки зрения Java, это будет выглядеть следующим образом:

> Как видите, мы просто написали обёртку над оригинальной функцией. В ФП каррирование как раз и представляет из себя простой и удобный способ создания обёрток. Вы сосредотачиваетесь на задаче, а компилятор пишет необходимый код за вас! Всё очень просто, и происходит каждый раз, когда вы хотите использовать паттерн Адаптер (обёртку).

Ленивые вычисления

String s1 = somewhatLongOperation1(); String s2 = somewhatLongOperation2(); String s3 = concatenate(s1, s2);
В императивных языках программирования очерёдность вычисления не вызывает никаких вопросов. Поскольку каждая функция может повлиять или зависеть от внешнего состояния, то необходимо соблюдать чёткую очерёдность вызовов: сначала somewhatLongOperation1 , затем somewhatLongOperation2 , и concatenate в конце. Но не всё так просто в функциональных языках.

Как мы уже видели ранее somewhatLongOperation1 и somewhatLongOperation2 могут быть запущены одновременно, потому что функции гарантированно не влияют и не зависят от глобального состояния. Но что, если мы не хотим выполнять их одновременно, нужно ли вызывать их последовательно? Ответ — нет. Эти вычисления должны быть запущены, только если какая-либо другая функция зависит от s1 и s2 . Нам даже не нужно выполнять их до тех пор, пока они понадобятся внутри concatenate . Если вместо concatenate мы подставим функцию, которая в зависимости от условия использует один аргумент из двух, то второй аргумент можно даже не вычислять! Haskell — это пример языка с отложенными вычислениями. В Haskell отсутствует гарантия какой-либо очередности вызовов (вообще!), потому что Haskell выполняет код по мере необходимости.

Ленивые вычисления обладают рядом достоинств как и некоторыми недостатками. В следующем разделе мы обсудим достоинства и я объясню как уживаться с недостатками.

Оптимизация

Абстрагирование структур управления

Unless(stock.isEuropean()) < sendToSEC(stock); >
Мы хотим, чтобы функция sendToSEC выполнялась только если фонд (stock) не европейский. Как можно реализовать unless ? Без ленивый вычислений нам бы понадобилась система макросов, но в языках, подобных Haskell, это не обязательно. Мы можем объявить unless в виде функции!

Void unless(boolean condition, List code) < if(!condition) code; >
Заметьте, что code не будет выполняться, если condition == true . В строгих языках такое поведение невозможно повторить, так как аргументы будут вычислены прежде, чем unless будет вызвана.

Бесконечные структуры данных

Недостатки


В ленивом языке никто не гарантирует, что первая строка выполнится раньше второй! Это означает, что мы не можем делать ввод-вывод, не можем нормально использовать нативные функции (ведь их нужно вызывать в определённом порядке, чтобы учитывать их побочные эффекты), и не можем взаимодействовать с внешним миром! Если мы введём механизм для упорядочивания выполнения кода, то потеряем преимущество математической строгости кода (а следом потеряем все плюшки функционального программирования). К счастью ещё не всё потеряно. Математики взялись за работу и придумали несколько приёмов для того, чтобы убедится в правильном порядке выполняемых инструкций не потеряв функционального духа. Мы получили лучшее от двух миров! Такие приёмы включают в себя продолжения (continuation), монады (monads) и однозначная типизация (uniqueness typing). В данной статье мы поработаем с продолжениями, а монады и однозначную типизацию отложим до следующего раза. Занятно, что продолжения очень полезная штука, которая используется не только для задания строгого порядка вычислений. Об этом мы тоже поговорим.

Продолжения

Когда мы рассматривали функции, мы изучили лишь половину правды, ведь мы исходили из предположения, что функция возвращает значение в вызывающую её функцию. В этом смысле продолжение — это обобщение функций. Функция не обязательно должна возвращать управление в то место, откуда её вызвали, а может возвращать в любое место программы. «Продолжение» — это параметр, который мы можем передать в функцию, чтобы указать точку возврата. Звучит намного страшнее, чем есть на самом деле. Давайте взглянем на следующий код:

Int i = add(5, 10); int j = square(i);
Функция add возвращает число 15, которое записывается в i , в том месте, где функция и была вызвана. Затем значение i используется при вызове square . Заметьте, что ленивый компилятор не может поменять очередность вычислений, ведь вторая строка зависит от результата первой. Мы можем переписать этот код с использованием Стиль Передачи Продолжения (Continuation Passing Style или CPS), когда add возвращает значение в функцию square .

Int j = add(5, 10, square);
В таком случае add получает дополнительный аргумент — функцию, которая будет вызвана после того, как add закончит работать. В обоих примерах j будет равен 225.

В этом и заключается первый приём, позволяющий задать порядок выполнения двух выражений. Вернёмся к нашему примеру с вводом-выводом

System.out.println(«Please enter your name: «); System.in.readLine();
Эти две строки не зависят друг от друга, и компилятор волен поменять их порядок по своему хотению. Но если мы перепишем в CPS, то тем самым добавим нужную зависимость, и компилятору придётся проводить вычисления одно за другим!

System.out.println(«Please enter your name: «, System.in.readLine);
В таком случае println должен будет вызвать readLine , передав ему свой результат, и вернуть результат readLine в конце. В таком виде мы можем быть уверены, что эти функции будут вызваны по очереди, и что readLine вообще вызовется (ведь компилятор ожидает получить результат последней операции). В случае Java println возвращает void . Но если бы возвращалось какое-либо абстрактное значение (которое может служить аргументом readLine), то это решило бы нашу проблему! Конечно выстраивание таких цепочек функций сильно ухудшает читаемость кода, но с этим можно бороться. Мы можем добавить в наш язык синтаксических плюшек, которые позволят нам писать выражения как обычно, а компилятор автоматически выстраивал бы вычисления в цепочки. Теперь мы можем проводить вычисления в любом порядке, не потеряв при этом достоинств ФП (включая возможность исследовать программу математическими методами)! Если вас это сбивает с толку, то помните, что функции — это всего лишь экземпляры класса с единственным членом. Перепишите наш пример так, чтобы println и readLine были экземплярами классов, так вам станет понятней.

Но на этом польза продолжений не заканчивается. Мы можем написать всю программу целиком используя CPS, чтобы каждая функция вызывалась с дополнительным параметром, продолжением, в которое передаётся результат. В принципе любую программу можно перевести на CPS, если воспринимать каждую функцию как частный случай продолжений. Такое преобразование можно произвести автоматически (в действительности многие компиляторы так и делают).

Как только мы переведём программу к CPS виду, становится ясно, что у каждой инструкции есть продолжение, функция в которую будет передаваться результат, что в обычной программе было бы точкой вызова. Возьмём любую инструкцию из последнего примера, например add(5,10) . В программе, написанной в CPS виде, понятно что будет являться продолжением — это функция, которую add вызовет по окончанию работы. Но что будет продолжением в случае не-CPS программы? Мы, конечно, можем конвертировать программу в CPS, но нужно ли это?

Оказывается, что в этом нет необходимости. Посмотрите внимательно на наше CPS преобразование. Если вы начнёте писать компилятор для него, то обнаружите, что для CPS версии не нужен стек! Функции никогда ничего не возвращают, в традиционном понимании слова «return», они просто вызывают другую функцию, подставляя результат вычислений. Отпадает необходимость проталкивать (push) аргументы в стек перед каждым вызовом, а потом извлекать (pop) их обратно. Мы можем просто хранить аргументы в каком-либо фиксированном участке памяти и использовать jump вместо обычного вызова. Нам нет нужны хранить первоначальные аргументы, ведь они больше никогда не понадобятся, ведь функции ничего не возвращают!

Таким образом, программы в CPS стиле не нуждаются в стеке, но содержат дополнительный аргумент, в виде функции, которую нужно вызвать. Программы в не-CPS стиле лишены дополнительного аргумента, но используют стек. Что же хранится в стеке? Просто аргументы и указатель на участок памяти, куда должна вернуться функция. Ну как, вы уже догадались? В стеке храниться информация о продолжениях! Указатель на точку возврата в стеке — это то же самое, что и функция, которую нужно вызвать, в CPS программах! Чтобы выяснить, какое продолжение у add(5,10) , достаточно взять из стека точку возврата.

Это было не трудно. Продолжение и указатель на точку возврата — это действительно одно и то же, только продолжение указывается явно, и по этому оно может отличаться от того места, где функция была вызвана. Если вы помните, что продолжение — это функция, а функция в нашем языке компилируется в экземпляр класса, то поймёте, что указатель на точку возврата в стеке и указатель на продолжение — это в действительности одно и то же, ведь наша функция (как экземпляр класса) — это всего лишь указатель. А значит, что в любой момент времени в вашей программы вы можете запросить текущее продолжение (по сути информацию из стека).

Хорошо, теперь мы уяснили, что же такое текущее продолжение. Что это значит? Если мы возьмём текущее продолжение и сохраним его где-нибудь, мы тем самым сохраним текущее состояние программы — заморозим её. Это похоже на режим гибернации ОС. В объекте продолжения хранится информация, необходимая для возобновления выполнения программы с той точки, когда был запрошен объект продолжения. Операционная система постоянно так делает с вашими программами, когда переключает контекст между потоками. Разница лишь в том, что всё находится под контролем ОС. Если вы запросите объект продолжения (в Scheme это делается вызовом функции call-with-current-continuation), то вы получите объект с текущим продолжением — стеком (или в случае CPS — функцией следующего вызова). Вы можете сохранить этот объект в переменную (или даже на диск). Если вы решите «перезапустить» программу с этим продолжением, то состояние вашей программы «преобразуется» к состоянию на момент взятия объекта продолжения. Это то же самое, как переключение к приостановленному потоку, или пробуждение ОС после гибернации. С тем исключением, что вы можете проделывать это много раз подряд. После пробуждения ОС информация о гибернации уничтожается. Если этого не делать, то можно было бы восстанавливать состояние ОС с одной и той же точки. Это почти как путешествие по времени. С продолжениями вы можете себе такое позволить!

В каких ситуациях продолжения будут полезны? Обычно если вы пытаетесь эмулировать состояние в системах лишенных такового по сути. Отличное применение продолжения нашли в Web-приложениях (например во фреймворке Seaside для языка Smalltalk). ASP.NET от Microsoft прикладывает огромные усилия, чтобы сохранять состояние между запросами, и облегчить вам жизнь. Если бы C# поддерживал продолжения, то сложность ASP.NET можно было бы уменьшить в два раза — достаточно было бы сохранять продолжение и восстанавливать его при следующем запросе. С точки зрения Web-программиста не было бы ни единого разрыва — программа продолжала бы свою работу со следующей строки! Продолжения — невероятно полезная абстракция для решения некоторых проблем. Учитывая то, что всё больше и больше традиционных толстых клиентов перемещаются в Web, важность продолжений будет со временем только расти.

Сопоставление с образцом (Pattern matching)

Давайте начнём наше знакомство с Pattern matching следующим примером. Вот функция вычисления чисел Фибоначи на Java:

Int fib(int n) < if(n == 0) return 1; if(n == 1) return 1; return fib(n - 2) + fib(n - 1); >
А вот пример на Java-подобном языке с поддержкой Pattern matching-а

Int fib(0) < return 1; >int fib(1) < return 1; >int fib(int n) < return fib(n - 2) + fib(n - 1); >
В чём разница? Компилятор реализует ветвление за нас.

Подумаешь, велика важность! Действительно важность не велика. Было подмечено, что большое количество функций содержат сложные switch конструкции (это отчасти верно для функциональных программ), и было принято решение выделить этот момент. Определение функции разбивается на несколько вариантов, и устанавливается паттерн на месте аргументов функции (это напоминает перегрузку методов). Когда происходит вызов функции, компилятор на лету сравнивает аргументы со всеми определениями и выбирает наиболее подходящий. Обычно выбор падает на самое специализированное определение функции. Например int fib(int n) может быть вызвана при n равном 1, но не будет, ведь int fib(1) — более специализированное определение.

Сопоставление с образцом обычно выглядит сложнее, чем в нашем примере. Например сложная система Pattern matching позволяет писать следующий код:

Энциклопедичный YouTube

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

Математика и константы / Введение в программирование, урок 4 (JavaScript ES6)

Реактивное программирование и современные веб-интерфейсы

Александр Чирцов о математике в физике

Анна Андреева. Решение олимпиадных задач по математике

Субтитры

Языки функционального программирования

Ещё не полностью функциональные изначальные версии и Лиспа , и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang , OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Wolfram (символьная математика), и (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Лисп , созданный Джоном Маккарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма функционального программирования . Дальнейшим развитием Лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций. [ ]

Функции высших порядков

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

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

Чистые функции

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

  • Если результат чистой функции не используется, её вызов может быть удален без вреда для других выражений.
  • Результат вызова чистой функции может быть мемоизирован , то есть сохранен в таблице значений вместе с аргументами вызова. Если в дальнейшем функция вызывается с этими же аргументами, её результат может быть взят прямо из таблицы, не вычисляясь (иногда это называется принципом прозрачности ссылок). Мемоизация , ценой небольшого расхода памяти, позволяет существенно увеличить производительность и уменьшить порядок роста некоторых рекурсивных алгоритмов.
  • Если нет никакой зависимости по данным между двумя чистыми функциями, то порядок их вычисления можно поменять или распараллелить (говоря иначе вычисление чистых функций удовлетворяет принципам thread-safe)
  • Если весь язык не допускает побочных эффектов, то можно использовать любую политику вычисления. Это предоставляет свободу компилятору комбинировать и реорганизовывать вычисление выражений в программе (например, исключить древовидные структуры).

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

Рекурсивные функции можно обобщить с помощью функций высших порядков, используя, например, катаморфизм и анаморфизм (или «свертка» и «развертка»). Функции такого рода играют роль такого понятия как цикл в императивных языках программирования. [ ]

Подход к вычислению аргументов

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

print (len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 — 4 ]))

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell . [ ]

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на Python. Другим примером является язык Ruby , который имеет возможность создания как lambda-объектов, так и возможность организации анонимных функций высшего порядка через блок с помощью конструкции yield.

Стили программирования

Императивные программы имеют склонность акцентировать последовательности шагов для выполнения какого-то действия, а функциональные программы к расположению и композиции функций, часто не обозначая точной последовательности шагов. Простой пример двух решений одной задачи (используется один и тот же язык Python) иллюстрирует это.

# императивный стиль target = # создать пустой список for item in source_list : # для каждого элемента исходного списка trans1 = G (item ) # применить функцию G() trans2 = F (trans1 ) # применить функцию F() target . append (trans2 ) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A , B : lambda x : A (B (x )) target = map (compose2 (F , G ), source_list )

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

Более точно, существует четыре ступени развития функционального стиля, в порядке убывания роли данных в программах:

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп , , Tcl , Rebol);
  • Комбинаторные (APL / / , FP / FL );
  • Бесточечные (чистые конкатенативные) (Joy , Cat , Factor , подмножество PostScript).

В первом случае вся структура программы определяется структурой данных, в последнем — данные как таковые вообще отсутствуют в исходном коде, они лишь подразумеваются на входе. Некоторые языки поддерживают ряд стилей: например, Haskell позволяет писать и в аппликативном, и в комбинаторном, и в бесточечном стилях.

Особенности

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

Сильные стороны

Повышение надёжности кода

Привлекательная сторона вычислений без состояний — повышение надёжности кода за счёт чёткой структуризации и отсутствия необходимости отслеживания побочных эффектов. Любая функция работает только с локальными данными и работает с ними всегда одинаково, независимо от того, где, как и при каких обстоятельствах она вызывается. Невозможность мутации данных при пользовании ими в разных местах программы исключает появление труднообнаруживаемых ошибок (таких, например, как случайное присваивание неверного значения глобальной переменной в императивной программе).

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

Недостатки функционального программирования вытекают из тех же самых его особенностей. Отсутствие присваиваний и замена их на порождение новых данных приводят к необходимости постоянного выделения и автоматического освобождения памяти, поэтому в системе исполнения функциональной программы обязательным компонентом становится высокоэффективный сборщик мусора . Нестрогая модель вычислений приводит к непредсказуемому порядку вызова функций, что создает проблемы при вводе-выводе, где порядок выполнения операций важен. Кроме того, очевидно, функции ввода в своем естественном виде (например, getchar из стандартной библиотеки языка ) не являются чистыми, поскольку способны возвращать различные значения для одних и тех же аргументов, и для устранения этого требуются определенные ухищрения.

Для преодоления недостатков функциональных программ уже первые языки функционального программирования включали не только чисто функциональные средства, но и механизмы императивного программирования (присваивание, цикл, «неявный PROGN» были уже в Лиспе). Использование таких средств позволяет решить некоторые практические проблемы, но означает отход от идей (и преимуществ) функционального программирования и написание императивных программ на функциональных языках. В чистых функциональных языках эти проблемы решаются другими средствами, например, в языке Haskell ввод-вывод реализован при помощи монад — нетривиальной концепции, позаимствованной из теории категорий.

Язык функционального программирования

В качестве основных свойств функциональных языков программирования обычно рассматриваются [кем? ] следующие:

Программы на функциональных языках обычно намного короче и проще, чем те же самые программы на императивных языках.
Пример (быстрая сортировка Хоара на абстрактном функциональном языке) :

QuickSort () =
quickSort () = quickSort (n | n t, n h)

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

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

В функциональных языках (равно как и вообще в языках программирования и математике) функции могут быть переданы другим функциям в качестве аргумента или возвращены в качестве результата. Функции, принимающие функциональные аргументы, называются функциями высших порядков или функционалами.

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

В традиционных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. В каком-то смысле противоположностью вызова-по-значению является вызов-по-необходимости (ленивые вычисления). В этом случае аргумент вычисляется, только если он нужен для вычисления результата.

Некоторые языки функционального программирования

Классификация функциональных языков

В качестве примера чистого функционального языка можно привести Haskell . Однако большинство функциональных языков являются гибридными и содержат свойства как функциональных, так и императивных языков. Яркие примеры — языки Scala и Nemerle. В них органично сочетаются характеристики как объектно-ориентированных языков, так и функциональных. Реализована хвостовая рекурсия и её оптимизация, функция является полноправным объектом, то есть может быть сохранена в переменной, передана в качестве аргумента в другую функцию или возвращена из функции.

Также функциональные языки делят на строгие и нестрогие . К нестрогим языкам относят те, которые поддерживают отложенные вычисления (F#), то есть аргументы функции вычисляются только тогда, когда они действительно понадобятся при вычислении функции. Ярким примером нестрогого языка является Haskell. В качестве примера строгого языка можно привести Standard ML .

Некоторые функциональные языки реализованы поверх платформообразующих виртуальных машин (JVM, .NET), то есть приложения на этих языках могут работать в среде времени исполнения (JRE, CLR) и использовать встроенные классы. К ним относятся Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

Ссылки

  • https://fprog.ru/ — Журнал «Практика функционального программирования»
  • https://www.intuit.ru/department/pl/funcpl/1/ — Основы функционального программирования. Л. В. Городняя
  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года;
  • https://alexott.net/ru/fp/books/ — Обзор литературы о функциональном программировании . Рассматриваются книги как на русском, так и на английском языке.

Wikimedia Foundation . 2010 .

Смотреть что такое «Язык функционального программирования» в других словарях:

язык прграммирования Лисп — Язык функционального программирования. Тематики информационные технологии в целом EN Lisp … Справочник технического переводчика

Универсальный язык программирования высокого уровня. Язык Лисп: относится к декларативным языкам функционального типа; предназначен для обработки символьных данных, представленных в виде списков. Основой языка являются функции и рекурсивные… … Финансовый словарь

У этого термина существуют и другие значения, см. Alice. Alice Семантика: функциональный Тип исполнения: компиляция в байткод для виртуальной машины Появился в: 2002 … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Oz Семантика: функциональный, процедурный, декларативный, объектно ориентированный, вычисления с ограничениями, Н модели, параллельные вычисления Тип исполнения: компилируемый Появился в: 1991 Автор(ы): Gert Smolka his students Релиз … Википедия

AWL (Alternative Web Language) Класс языка: мультипарадигмальный: функциональный, процедурный, объектно ориентированный Тип исполнения: интерпретируемый Появился в: 2005 г. Типизация данных: динамическая … Википедия

У этого термина существуют и другие значения, см. Леда (значения). Леда (Leda) мультипарадигмальный язык программирования, спроектированный Тимоти Баддом. Язык Leda исходно создавался с целью совмещения императивного программирования, объектно… … Википедия

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

В языках функционального программирования основным конструктивным элементом является математическое понятие функции. Существует различия в понимании функции в математике и функции в программировании, в следствии чего нельзя отнести Си подобные… … Википедия

Python был задуман в 1980 х годах, а его создание началось в декабре 1989 года Гвидо ван Россумом в составе центра математики и информатики в Нидерландах. Язык Python был задуман как потомок языка программирования ABC, способный к обработке… … Википедия

Принципы функционального программирования: почему это важно. Функциональный язык программирования

Функциональное программирование объединяет разные подходы к определению процессов вычисления на основе достаточно строгих абстрактных понятий и методов символьной обработки данных.

Особенностью языков функционального программирования является то, что тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. Основные свойства функциональных языков программирования: краткость, простота, строгая типизация, модульность, наличие отложенных (ленивых) вычислений.

К функциональным языкам программирования относят: Lisp, Miranda, Gofel, ML, Standard ML, Objective CAML, F#, Scala, Пифагор и др.

Процедурные языки программирования

Процедурный язык программирования предоставляет возможность программисту определять каждый шаг в процессе решения задачи. Особенность таких языков программирования состоит в том, что задачи разбиваются на шаги и решаются шаг за шагом. Используя процедурный язык, программист определяет языковые конструкции для выполнения последовательности алгоритмических шагов.

Процедурные языки программирования: Ada, Basic, Си, КОБОЛ, Pascal, ПЛ/1, Рапира и др.

Стековые языки программирования

Стековый язык программирования − это язык программирования, в котором для передачи параметров используется машинная модель стека. Стековые языки программирования: Forth, PostScript, Java, C# и др. При использовании стека, в качестве основного канала передачи параметров между словами, элементы языка, естественным образом, образуют фразы (последовательное сцепление). Это свойство сближает данные языки с естественными языками.

Аспектно-ориентированные языки программирования 5) Декларативные языки программирования 6) Динамические языки программирования 7) Учебные языки программирования 8) Языки описания интерфейсов 9) Языки прототипного программирования 10) Объектно-ориентированные языки программирования 11) Логические языки программирования 12) Сценарные языки программирования 13) Эзотерические языки программирования

Стандартизация языков программирования. Парадигма программирования

Концепция языка программирования неотрывно связана с его реализацией. Для того чтобы компиляция одной и той же программы различными компиляторами всегда давала одинаковый результат, разрабатываются стандарты языков программирования. Организации, занимающиеся вопросами стандартизации: Американский национальный институт стандартов ANSI, Институт инженеров по электротехнике и электронике IEEE, Организация международных стандартов ISO.

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

Парадигма – набор теорий, стандартов и методов, которые совместно представляют собой способ организации научного знания, – иными словами, способ видения мира. По аналогии с этим принято считать, что парадигма в программировании – способ концептуализации, который определяет, как следует проводить вычисления, и как работа, выполняемая компьютером, должна быть структурирована и организована.

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

C и Pascal являются примерами языков, предназначенных для директивного программирования, когда разработчик программы использует процессно-ориентированная модель, то есть пытается создать код, должным образом воздействующий на данные. Активным началом при этом подходе считается программа (код), которая должна выполнить все необходимые для достижения нужного результата действия над пассивными данными.

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

Программирование – процесс создания компьютерных программ. В более широком смысле: спектр деят-сти, связ-ый с созданием и поддержанием в раб. состоянии программ — ПО ЭВМ.

Технология программирования — совокупность методов и средств, используемых в процессе разработки программного обеспечения.

Технология программир-я представляет собой набор технологических инструкций, включающих:

· указание последоват-сти выполнения технологич-х операций;

· перечисление условий, при кот-х выполняется та или иная операция;

· описания самих операций, где для каждой операции определены исходные данные, результаты, а также инструкции, нормативы, стандарты, критерии и т. п.

Современная технология программирования — компонентный подход , который предполагает построение программного обеспечения из отдельных компонентов — физически отдельно существующих частей программного обеспечения, которые взаимодействуют между собой через стандартизованные двоичные интерфейсы. В настоящее время критериями качества программного продукта принято считать:− функциональность ; − надежность ;− легкость применения ;− эффективность (отношение уровня услуг, предоставл-х программным продуктом пользов-лю при заданных условиях, к объему используемых ресурсов);− сопровождаемость (характер-ки программ-го продукта, которые позволяют минимизир-ть усилия по внесению изменений для устранения в нем ошибок и по его модификации в соотв-вии с изменяющ-ся потребностями пользов-лей);− мобильность (способность ПС быть перенесенным из одной среды в другую, в частности, с одной ЭВМ на др.).

Важным этапом создания прогр-го продукта явл. тестирование и отладка.

Отладка − это деятельность, направленная на обнаружение и исправление ошибок в программном продукте с использованием процессов выполнения его программ.

Тестирование − это процесс выполнения его программ на некотором наборе данных, для которого заранее известен результат применения или известны правила поведения этих программ.

Существуют следующие методы тестирования ПС:

1) Статическое тестирование – ручная проверка программы за столом.

2) Детерминированное тестир-е – при разл-х комбинациях исх-х данных.

3) Стохастическое – исх. данные выбир-ся произвольно, на выходе определяется качеств-е совпадение результатов или примерная оценка.

Стиль программирования — набор приемов или методов программирования, которые используют программисты, чтобы получить правильные, эффективные, удобные для применения и легкочитаемые программы.

Существует несколько стилей программирования:

  1. Процедурное программирование – это программирование, при котором программа представляет собой последовательность операторов. Используется в языках высокого уровня Basic, Fortran и др.
  2. Функциональное программирование – это программирование, при котором программа представляет собой последовательность вызовов функций. Используется в языках Lisp и др.
  3. Логическоепрограммирование – это программирование, при котором программа представляет собой совокупность определения соотношений между объектами. Используется в языках Prolog и др.

Объектно-ориентированноепрограммирование – это программирование, при котором основой программы является объект представляющий собой совокупность данных и правил их преобразования. Используется в языках Turbo-Pascal, C++ и др.

На практике отличие математической функции от понятия «функции» в императивном программировании заключается в том, что императивные функции могут опираться не только на аргументы, но и на состояние внешних по отношению к функции переменных, а также иметь побочные эффекты и менять состояние внешних переменных. Таким образом, в императивном программировании при вызове одной и той же функции с одинаковыми параметрами, но на разных этапах выполнения алгоритма, можно получить разные данные на выходе из-за влияния на функцию состояния переменных. А в функциональном языке при вызове функции с одними и теми же аргументами мы всегда получим одинаковый результат: выходные данные зависят только от входных. Это позволяет средам выполнения программ на функциональных языках кешировать результаты функций и вызывать их в порядке, не определяемом алгоритмом и распараллеливать их без каких-либо дополнительных действий со стороны программиста (что обеспечивают функции без побочных эффектов — чистые функции ).

Энциклопедичный YouTube

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

Математика и константы / Введение в программирование, урок 4 (JavaScript ES6)

Реактивное программирование и современные веб-интерфейсы

Александр Чирцов о математике в физике

Анна Андреева. Решение олимпиадных задач по математике

Субтитры

Языки функционального программирования

Ещё не полностью функциональные изначальные версии и Лиспа , и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang , OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Wolfram (символьная математика), и (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Лисп , созданный Джоном Маккарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма функционального программирования . Дальнейшим развитием Лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций. [ ]

Функции высших порядков

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

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

Чистые функции

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

  • Если результат чистой функции не используется, её вызов может быть удален без вреда для других выражений.
  • Результат вызова чистой функции может быть мемоизирован , то есть сохранен в таблице значений вместе с аргументами вызова. Если в дальнейшем функция вызывается с этими же аргументами, её результат может быть взят прямо из таблицы, не вычисляясь (иногда это называется принципом прозрачности ссылок). Мемоизация , ценой небольшого расхода памяти, позволяет существенно увеличить производительность и уменьшить порядок роста некоторых рекурсивных алгоритмов.
  • Если нет никакой зависимости по данным между двумя чистыми функциями, то порядок их вычисления можно поменять или распараллелить (говоря иначе вычисление чистых функций удовлетворяет принципам thread-safe)
  • Если весь язык не допускает побочных эффектов, то можно использовать любую политику вычисления. Это предоставляет свободу компилятору комбинировать и реорганизовывать вычисление выражений в программе (например, исключить древовидные структуры).

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

Рекурсивные функции можно обобщить с помощью функций высших порядков, используя, например, катаморфизм и анаморфизм (или «свертка» и «развертка»). Функции такого рода играют роль такого понятия как цикл в императивных языках программирования. [ ]

Подход к вычислению аргументов

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

print (len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 — 4 ]))

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell . [ ]

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на Python. Другим примером является язык Ruby , который имеет возможность создания как lambda-объектов, так и возможность организации анонимных функций высшего порядка через блок с помощью конструкции yield.

Стили программирования

Императивные программы имеют склонность акцентировать последовательности шагов для выполнения какого-то действия, а функциональные программы к расположению и композиции функций, часто не обозначая точной последовательности шагов. Простой пример двух решений одной задачи (используется один и тот же язык Python) иллюстрирует это.

# императивный стиль target = # создать пустой список for item in source_list : # для каждого элемента исходного списка trans1 = G (item ) # применить функцию G() trans2 = F (trans1 ) # применить функцию F() target . append (trans2 ) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A , B : lambda x : A (B (x )) target = map (compose2 (F , G ), source_list )

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

Более точно, существует четыре ступени развития функционального стиля, в порядке убывания роли данных в программах:

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп , , Tcl , Rebol);
  • Комбинаторные (APL / / , FP / FL );
  • Бесточечные (чистые конкатенативные) (Joy , Cat , Factor , подмножество PostScript).

В первом случае вся структура программы определяется структурой данных, в последнем — данные как таковые вообще отсутствуют в исходном коде, они лишь подразумеваются на входе. Некоторые языки поддерживают ряд стилей: например, Haskell позволяет писать и в аппликативном, и в комбинаторном, и в бесточечном стилях.

Особенности

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

Сильные стороны

Повышение надёжности кода

Привлекательная сторона вычислений без состояний — повышение надёжности кода за счёт чёткой структуризации и отсутствия необходимости отслеживания побочных эффектов. Любая функция работает только с локальными данными и работает с ними всегда одинаково, независимо от того, где, как и при каких обстоятельствах она вызывается. Невозможность мутации данных при пользовании ими в разных местах программы исключает появление труднообнаруживаемых ошибок (таких, например, как случайное присваивание неверного значения глобальной переменной в императивной программе).

Мастер Йода рекомендует:  Лекция 1. Что такое ASP.NET. Инсталляция и тестовый проект.

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

Недостатки функционального программирования вытекают из тех же самых его особенностей. Отсутствие присваиваний и замена их на порождение новых данных приводят к необходимости постоянного выделения и автоматического освобождения памяти, поэтому в системе исполнения функциональной программы обязательным компонентом становится высокоэффективный сборщик мусора . Нестрогая модель вычислений приводит к непредсказуемому порядку вызова функций, что создает проблемы при вводе-выводе, где порядок выполнения операций важен. Кроме того, очевидно, функции ввода в своем естественном виде (например, getchar из стандартной библиотеки языка ) не являются чистыми, поскольку способны возвращать различные значения для одних и тех же аргументов, и для устранения этого требуются определенные ухищрения.

Для преодоления недостатков функциональных программ уже первые языки функционального программирования включали не только чисто функциональные средства, но и механизмы императивного программирования (присваивание, цикл, «неявный PROGN» были уже в Лиспе). Использование таких средств позволяет решить некоторые практические проблемы, но означает отход от идей (и преимуществ) функционального программирования и написание императивных программ на функциональных языках. В чистых функциональных языках эти проблемы решаются другими средствами, например, в языке Haskell ввод-вывод реализован при помощи монад — нетривиальной концепции, позаимствованной из теории категорий.

В традиционных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. В каком-то смысле противоположностью вызова-по-значению является вызов-по-необходимости. В этом случае аргумент вычисляется, только если он нужен для вычисления результата. Примером такого поведения можно взять оператор конъюнкции всё из того же C++ (&&), который не вычисляет значение второго аргумента, если первый аргумент имеет ложное значение.

Если функциональный язык не поддерживает отложенные вычисления, то он называется строгим. На самом деле, в таких языках порядок вычисления строго определен. В качестве примера строгих языков можно привести Scheme, Standard ML и Caml.

Языки, использующие отложенные вычисления, называются нестрогими. Haskell — нестрогий язык, так же как, например, Gofer и Miranda. Нестрогие языки зачастую являются чистыми.

Очень часто строгие языки включают в себя средства поддержки некоторых полезных возможностей, присущих нестрогим языкам, например бесконечных списков. В поставке Standard ML присутствует специальный модуль для поддержки отложенных вычислений. А Objective Caml помимо этого поддерживает дополнительное зарезервированное слово lazy и конструкцию для списков значений, вычисляемых по необходимости.

В этом разделе приведено краткое описание некоторых языков функционального программирования (очень немногих).

§ Lisp (List processor). Считается первым функциональным языком программирования. Нетипизирован. Содержит массу императивных свойств, однако в общем поощряет именно функциональный стиль программирования. При вычислениях использует вызов-по-значению. Существует объектно-ориентированный диалект языка — CLOS.

§ ISWIM (If you See What I Mean). Функциональный язык-прототип. Разработан Ландиным в 60-х годах XX века для демонстрации того, каким может быть язык функционального программирования. Вместе с языком Ландин разработал и специальную виртуальную машину для исполнения программ на ISWIM’е. Эта виртуальная машина, основанная на вызове-по-значению, получила название SECD-машины. На синтаксисе языка ISWIM базируется синтаксис многих функциональных языков. На синтаксис ISWIM похож синтаксис ML, особенно Caml.

§ Scheme . Диалект Lisp’а, предназначенный для научных исследований в области computer science. При разработке Scheme был сделан упор на элегантность и простоту языка. Благодаря этому язык получился намного меньше, чем Common Lisp.

§ ML (Meta Language). Семейство строгих языков с развитой полиморфной системой типов и параметризуемыми модулями. ML преподается во многих западных университетах (в некоторых даже как первый язык программирования).

§ Standard ML . Один из первых типизированных языков функционального программирования. Содержит некоторые императивные свойства, такие как ссылки на изменяемые значения и поэтому не является чистым. При вычислениях использует вызов-по-значению. Очень интересная реализация модульности. Мощная полиморфная система типов. Последний стандарт языка — Standard ML-97, для которого существует формальные математические определения синтаксиса, а также статической и динамической семантик языка.

§ Caml Light и Objective Caml . Как и Standard ML принадлежит к семейству ML. Objective Caml отличается от Caml Light в основном поддержкой классического объектно-ориентированного программирования. Также как и Standard ML строгий, но имеет некоторую встроенную поддержку отложенных вычислений.

§ Miranda . Разработан Дэвидом Тернером, в качестве стандартного функционального языка, использовавшего отложенные вычисления. Имеет строгую полиморфную систему типов. Как и ML преподаётся во многих университетах. Оказал большое влияние на разработчиков языка Haskell.

§ Haskell . Один из самых распространённых нестрогих языков. Имеет очень развитую систему типизации. Несколько хуже разработана система модулей. Последний стандарт языка — Haskell-98.

§ Gofer (GOod For Equational Reasoning). Упрощённый диалект Haskell’а. Предназначен для обучения функциональному программированию.

§ Clean . Специально предназначен для параллельного и распределённого программирования. По синтаксису напоминает Haskell. Чистый. Использует отложенные вычисления. С компилятором поставляется набор библиотек (I/O libraries), позволяющих программировать графический пользовательский интерфейс под Win32 или MacOS.

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

Функциональный подход породил целое семейство языков, родоначальником которых, как уже отмечалось, стал язык программирования LISP. Позднее, в 70-х годах, был разработан первоначальный вариант языка ML, который впоследствии развился, в частности, в SML, а также ряд других языков. Из них, пожалуй, самым «молодым» является созданный уже совсем недавно, в 90-х годах, язык Haskell.

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

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

Поскольку функция является естественным формализмом для языков функционального программирования, реализация различных аспектов программирования, связанных с функциями, существенно упрощается. Интуитивно прозрачным становится написание рекурсивных функций, т.е. функций, вызывающих самих себя в качестве аргумента. Естественной становится и реализация обработки рекурсивных структур данных.

Благодаря реализации механизма сопоставления с образцом, такие языки функционального программирования как ML и Haskell хорошо использовать для символьной обработки.

Естественно, языки функционального программирования не лишены и некоторых недостатков.

Часто к ним относят нелинейную структуру программы и относительно невысокую эффективность реализации. Однако первый недостаток достаточно субъективен, а второй успешно преодолен современными реализациями, в частности, рядом последних трансляторов языка SML, включая и компилятор для среды Microsoft .NET.

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

Заметим, что под термином «функция» в математической формализации и программной реализации имеются в виду различные понятия.

Так, математической функцией f с областью определения A и областью значений B называется множество упорядоченных пар

(a,b 1) f и (a,b 2) f,

В свою очередь, функцией в языке программирования называется конструкция этого языка, описывающая правила преобразования аргумента (так называемого фактического параметра) в результат.

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

Под конверсией понимается преобразование объектов исчисления (а в программировании – функций и данных) из одной формы в другую. Исходной задачей в математике было стремление к упрощению формы выражений. В программировании именно эта задача не является столь существенной, хотя, как мы увидим в дальнейшем, использование лямбда-исчисления как исходной формализации может способствовать упрощению вида программы, т.е. вести к оптимизации программного кода.

Кроме того, конверсии обеспечивают переход к вновь введенным обозначениям и, таким образом, позволяют представлять предметную область в более компактном либо более детальном виде, или, говоря математическим языком, изменять уровень абстракции по отношению к предметной области. Эту возможность широко используют также языки объектно-ориентированного и структурно-модульного программирования в иерархии объектов, фрагментов программ и структур данных. На этом же принципе основано взаимодействие компонентов приложения в.NET. Именно в этом смысле переход к новым обозначениям является одним из важнейших элементов программирования в целом, и именно лямбда-исчисление (в отличие от многих других разделов математики) представляет собой адекватный способ формализации переобозначений.

Систематизируем эволюцию теорий, лежащих в основе современного подхода к лямбда-исчислению.

Рассмотрим эволюцию языков программирования, развивающихся в рамках функционального подхода.

Ранние языки функционального программирования, которые берут свое начало от классического языка LISP (LISt Processing), были предназначены, для обработки списков, т.е. символьной информации. При этом основными типами были атомарный элемент и список из атомарных элементов, а основной акцент делался на анализе содержимого списка.

Развитием ранних языков программирования стали языки функционального программирования с сильной типизацией, характерным примером здесь является классический ML, и его прямой потомок SML. В языках с сильной типизацией каждая конструкция (или выражение) должна иметь тип.

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

Следующим шагом в развитии языков функционального программирования стала поддержка полиморфных функций, т.е. функций с параметрическими аргументами (аналогами математической функции с параметрами). В частности, полиморфизм поддерживается в языках SML, Miranda и Haskell.

На современном этапе развития возникли языки функционального программирования «нового поколения» со следующими расширенными возможностями: сопоставление с образцом (Scheme, SML, Miranda, Haskell), параметрический полиморфизм (SML) и так называемые «ленивые» (по мере необходимости) вычисления (Haskell, Miranda, SML).

Семейство языков функционального программирования довольно многочисленно. Об этом свидетельствует не столько значительный список языков, сколько тот факт, что многие языки дали начало целым направлениям в программировании. Напомним, что LISP дал начало целому семейству языков: Scheme, InterLisp, COMMON Lisp и др.

Не стал исключением и язык программирования SML, который был создан в форме языка ML Р. Милнером (Robin Milner) в MIT (Massachusetts Institute of Technology) и первоначально предназначен для логических выводов, в частности, доказательства теорем. Язык отличается строгой типизацией, в нем отсутствует параметрический полиморфизм.

Развитием «классического» ML стали сразу три современных языка с практически одинаковыми возможностями (параметрический полиморфизм, сопоставление с образцом, «ленивые» вычисления). Это язык SML, разработанный в Великобритании и США, CaML, созданный группой французских ученых института INRIA, SML/NJ – диалект SML из New Jersey, а также российская разработка – mosml («московский» диалект ML).

Близость к математической формализации и изначальная функциональная ориентированность послужили причиной следующих преимуществ функционального подхода:

1. простота тестирования и верификации программного кода на основе возможности построения строгого математического доказательства корректности программ;

2. унификация представления программы и данных (данные могут быть инкапсулированы в программу как аргументы функций, означивание или вычисление значения функции может производиться по мере необходимости);

3. безопасная типизация: недопустимые операции с данными исключены;

4. динамическая типизация: возможно обнаружение ошибок типизации во время выполнения (отсутствие этого свойства в ранних языках функционального программирования может приводить к переполнению оперативной памяти компьютера);

5. независимость программной реализации от машинного представления данных и системной архитектуры программы (программист сосредоточен на деталях реализации, а не на особенностях машинного представления данных).

Заметим, что реализация преимуществ, которые предоставляют языки функционального программирования, существенно зависит от выбора программно-аппаратной платформы.

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

1. интеграция различных языков функционального программирования (при этом максимально используются преимущества каждого из языков, в частности, Scheme предоставляет механизм сопоставления с образцом, а SML – возможность вычисления по мере необходимости);

2. интеграция различных подходов к программированию на основе межъязыковой инфраструктуры Common Language Infrastructure, или CLI (в частности, возможно использование C# для обеспечения преимуществ объектно-ориентированного подхода и SML – функционального, как в настоящем курсе);

3. общая унифицированная система типизации Common Type System, CTS (единообразное и безопасное управление типами данных в программе);

4. многоступенчатая, гибкая система обеспечения безопасности программного кода (в частности, на основе механизма сборок).

Основными особенностями функциональных языков программирования, отличающими их как от императивных языков, так и от языков логического программирования, являются прозрачность по ссылкам и детерминизм. В функциональных языках существует значительный разброс по таким параметрам как типизация, правила вычисления. Во многих языках порядок вычисления строго определен. Но иногда строгие языки содержат средства поддержки некоторых полезных элементов, присущих нестрогим языкам, например бесконечных списков (в Standard ML присутствует специальный модуль для поддержки отложенных вычислений). Напротив, нестрогие языки позволяют в некоторых случаях выполнять энергичные вычисления.

Так, Miranda имеет ленивую семантику, но позволяет специфицировать строгие конструкторы, пометив определенным образом аргументы конструктора.

Многие современные языки функционального программирования являются строго типизированными языками (строгая типизация). Строгая типизация обеспечивает большую безопасность. Многие ошибки могут быть исправлены на стадии компиляции, поэтому стадия отладки и общее время разработки программ сокращаются. Строгая типизация позволяет компилятору генерировать более эффективный код и тем самым ускорять выполнение программ. Наряду с этим, существуют функциональные языки с динамической типизацией. Тип данных в таких языках определяется во время выполнения программы (гл. 3). Иногда их называют «безтиповыми». К их, достоинствам следует отнести то, что программы, написанные на этих языках, обладают большей общностью. Недостатком можно считать отнесение многих ошибок на стадию выполнения программы и связанную с этим необходимость применения функций проверки типов и соответствующее сокращение общности программы. Типизированные языки способствуют генерации более «надежного» кода, а типизированные более «общего».

Следующим критерием, по которому можно провести классификацию функциональных языков программирования, может стать наличие императивных механизмов. При этом принято называть функциональные языки программирования, лишенные императивных механизмов, «чистыми», а имеющие их – «нечистыми». В обзоре функциональных языков программирования, приведенном ниже, языки программирования будут называться «практическими» и «академическими». Под «практическими» языками понимаются языки, имеющие коммерческое приложение (на них разрабатывались реальные приложения или были коммерческие системы программирования). Академические языки программирования имеют популярность в исследовательских кругах и в области компьютерного образования, но коммерческих приложений, написанных на таких языках, практически нет. Они остаются всего лишь инструментом при проведении теоретических исследований в области информатики и широко используются в образовательном процессе.

Перечень наиболее популярных функциональных языков программирования приводится ниже с использованием следующих критериев: общие сведения; типизация; вид вычисления; чистота.

Common Lisp. Версия Лиспа, которая с 1970 г. может считаться стандартом языка, благодаря поддержке со стороны лаборатории искусственного интеллекта Массачусетского технологического института, безтиповый, энергичный, с большим набором императивных включений, допускающих присваивание, разрушение структур. Практический. Достаточно сказать, что на Лиспе был написан векторный графический редактор Автокад.

Scheme. Диалект Лиспа, предназначенный для научных исследований в области компьютерной науки и обучения функциональному программированию. Благодаря отсутствию императивных включений язык получился намного меньше, чем Common Lisp. Восходит к языку, разработанному Дж. Маккарти в 1962 г. Академический, безтиповый, энергичный, чистый.

Refal. Семейство языков, разработанных В. Ф. Турчиным. Старейший член этого семейства впервые реализован в 1968 году в России. Широко используется и поныне в академических кругах. Содержит элементы логического программирования (сопоставление с образцом). Поэтому язык Refal предлагается в данном учебном пособии в качестве языка для самостоятельного изучения.

Miranda. Строго типизированный, поддерживает типы данных пользователя и полиморфизм. Разработан Тернером на основе более ранних языков SALS и KRC. Имеет ленивую семантику. Без императивных включений.

Haskell. Развитие языка пришлось на конец прошлого века. Широко известен в академических кругах. В некоторых западных университетах используется в качестве основного языка для изучения студентами. Один из наиболее мощных функциональных языков. Ленивый язык. Чисто функциональный язык. Типизированный. Haskell – отличный инструмент для обучения и экспериментов со сложными функциональными типами данных. Программы, написанные на Haskell, имеют значительный размер объектного кода и невысокую скорость исполнения.

Clean. Диалект Haskell, приспособленный к нуждам практического программирования. Как и Haskell, является ленивым чисто функциональным языком, содержит классы типов. Но Clean также содержит интересные особенности, которые не имеют эквивалента в Haskell. Например, императивные возможности в Clean основаны на уникальных типах, идея которых заимствована из линейной логики (linear logic). Clean содержит механизмы, которые позволяют значительно улучшить эффективность программ. Сред этих механизмов явное подавление отложенных вычислений. Реализация Clean является коммерческим продуктом, но свободная версия доступна для исследовательских и образовательных целей.

ML(Meta Language). Разработан группой программистов во главе с Робертом Милиером в середине 70-х гг. в Эдинбурге (Edinburgh Logic for Computable Functions). Идея языка состояла в создании механизма для построения формальных доказательств в системе логики для вычислимых функций. В 1983 язык был пересмотрен дополнен такими концепциями, как модули. Стал называться стандартный ML. ML – это сильно типизированный язык со статическим контролем типов и аппликативным выполнением программ. Он завоевал большую популярность в исследовательских кругах и в области компьютерного образования.

Множество X называют областью определения функции П а множество У — областью значений функции П Величина х в Р(х), которая представляет собой любой элемент из множества X, называется независимой переменной, а величину у из множества У, определяемую уравнением у = Р(х), называют зависимой переменной. Иногда, если функция f не определена для всех х в X, говорят о частично определенной функции, в противном случае имеют в виду полное определение.

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

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

В языках программирования совершенно четко разделены определение функции и применение функции. Определение функции описывает, как можно вычислить величину на основе формальных параметров. Применение функции заключается в вызове конкретной функции с использованием фактических параметров. Заметим, что в математике разница между параметром и переменной не всегда очевидна. Очень часто термин «параметр» заменяют термином «независимая переменная». К примеру, в математике можно записать определение функции возведения в квадрат: square(x) = х * х

Положим, что для х выполняется square(x) + 2 . . .

Основное различие между императивным и функциональным программированием состоит в интерпретации понятия переменной . В математике переменные представляются как фактические значения, а в императивных языках программирования переменные ссылаются на области памяти, где хранятся их значения. Изменить значения в этих областях памяти позволяют присваивания. Напротив, в математике нет понятий «область памяти» и «присваивание», поэтому такой оператор, как х = х + 1

не имеет смысла. Функциональное программирование основывается на математическом подходе к понятию «переменная». В функциональном программировании переменные связаны со значениями, но не имеют никакого отношения к областям памяти. После привязывания переменной к значению значение переменной меняться

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным.

Основные концепции

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

Чистые функции

Чистая функция максимально проста. Она должна всегда возвращать один и тот же результат. Посмотрите на эту JavaScript-функцию:

var z = 10; function add(x, y)

function add (x , y ) <

Конечно, это искусственный пример, но он очень хорошо демонстрирует, как должна работать чистая функция. При одних и тех же значениях x и y мы всегда будем получать одинаковый результат функции. Предсказуемость – важная часть работы программы в функциональном программировании.

Изменяемые данные и побочные эффекты

Вернемся к примеру кода. Если мы добавим в качестве аргумента функции add() , переменную z , которая объявлена выше, наша функция перестанет быть чистой и предсказуемой. Почему? Потому что z объявлена как обычная переменная: она доступна для изменения из любого места программы.

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

Корректный код чистой функции с z должен выглядеть так:

const x = 10; const z = 10; add (x, z); // вернет 20

add (x , z ) ; // вернет 20

В этом случае функция всегда будет возвращать предсказуемый результат. Если функция не будет работать предсказуемо – это приведет к нежелательным побочным эффектам.

Еще один пример не функционального кода – классические циклы. Вспомним, как выглядит типичный цикл for в JavaScript:

var acc = 0; for (var i = 1; i end) < return acc; >else < return sumRange(start + 1, end, acc + start); >> console.log(sumRange(1, 10, 0)); // выведет 55

function sumRange (start , end , acc ) <

return sumRange (start + 1 , end , acc + start ) ;

console . log (sumRange (1 , 10 , 0 ) ) ; // выведет 55


Такая конструкция позволяет использовать константы для определения начала, конца цикла и шага. В основе такого типа цикла лежит идея вызова функции внутри себя, или рекурсивного вызова. В примере выше функция sumRange() с заданными аргументами делает проверку условия, и в случае ложного результата вызывает саму себя с измененными аргументами.

Композиция функций

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

function addOne(x) < return x + 1; >function timesTwo(x) < return x * 2; >console.log(addOne(timesTwo(3))); // выведет 7 console.log(timesTwo(addOne(3))); // выведет 8

function addOne (x ) <

function timesTwo (x ) <

В примере выше мы описали две простые функции: addOne (прибавляет к аргументу единицу) и timesTwo (умножает аргумент на два). Техника компоновки позволяет нам вызывать две эти функции друг в друге в разном порядке. В результате, с разным логическим порядком вызова чистых функций и одинаковым значением аргумента мы получили более сложный функционал, который дает нам необходимый результат и делает это предсказуемо.

Польза функционального программирования

Функциональное программирование помогает сделать код более чистым, предсказуемым и легко читаемым. Использование принципов ФП помогает избавиться от лишних абстракций с непредсказуемым поведением, следовательно, сделать работу программы более предсказуемой и снизить количество возможных ошибок.

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

Функциональное программирование плохо подходит для алгоритмов, основанных на графах из-за сравнительно более медленной работы программы. ФП в целом плохо применимо в решениях, которые годами основывались на использовании императивного подхода.

Самое важное – в функциональных языках нет эффективного неупорядоченного словаря множества. В сравнении с хэш-таблицами, функциональные словари работают значительно хуже, а это может быть критично для некоторых приложений. Однако указанные недостатки можно отнести только к императивным языкам.

Функциональное программирование в языках

Так как функциональное программирование – это прежде всего подход к написанию кода, использовать его принципы можно в любом языке. Однако существуют языки, специально заточенные под функциональный подход. Первый и самый известный из них – Lisp. Он появился еще в 1958 году. Его автор – Джон Маккарти, информатик и автор термина «искусственный интеллект». Lisp по сей день популярен в среде проектировщиков ИИ.

Более современные функциональные языки, такие как Elm и Elixir, по данным GitHub и Stack Overflow постепенно и уверенно набирают популярность. Рост популярности JavaScript также привел к повышенному интересу к концепциям функционального программирования для применения в этом языке.

�� Принципы функционального программирования: почему это важно

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным.

Принципы функционального программирования: почему это важно | Библиотека программиста

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным. Основные концепции Функциональное

Комментарии (69)

Денис Кузнецов

В описании концепции какая-то фигня. То есть, они приписывают это к функциональному программированию, а по факту — так в принципе, ВСЕГДА, при любом условии, именно так нужно писать код — четко, понятно, без непредсказуемых результатов. Что-то мне подсказывает, что автор просто не понимает то, о чем пишет

Ян Шкуринский

Денис, тут просто, очевидно, вырожденные примеры, которые в реальной жизни выглядят нелепо. Думаю, просто хотел явно подчеркнуть разницу

Михаил Коротков

Очень слабая статья. Выходит, программируя, хм. на turbo pascal-е, если я напишу рекурсивную реализацию алгоритма без побочных эффектов, то могу ходить грудь колесом и кричать о том, что программирую. В ФУНКЦИОНАЛЬНОМ СТИЛЕ. (нет)

Павел Варакута

Спасибо предыдущим комментаторам за сэкономленное время)))

Павел Варакута

Михаил, рекурсия вообще зло! это как минимум дополнительный отжор памяти и неочевидное поведение. Там где можно избежать рекурсии — ее надо избегать. Мало того в софте NASA, BOING и много где еще, рекурсия запрещена) Мало того, рекурсия это всегда алгоритмы с минимум О(N)^2 сложностью. А без них часто можно добиться О(log N)

Михаил Коротков

Павелъ, полная ерунда. Любой алгоритм, реализованный в рекурсивной форме, может быть переписан в итерационном виде и наоборот. Это уже давно доказано. При оценке сложности алгоритмов не учитывают в какой форме будет реализация — в рекурсивной или итерационной. «Мало того в софте NASA, BOING и много где еще, рекурсия запрещена)» обычно, такие громкие заявления снабжают пруфами.

Юрий Морозов

Оч.хорошо, а скажите мне, свалившемуся с Луны, какое ещё программирование (ну, кроме упомянутого функционального) бывает?

Александр Гранин

Павел, не нужно распространять неправду. — Почти все алгоритмы на деревьях — рекурсивные, и многие так и исполнены в библиотеках. Там О(logN), O(N) и O(1) — Рекурсия требует стек. Стек в любой мало-мальски сложной программе и так используется глубоко. — Существует Tail Call Optimization. — В функциональных языках стек как понятие может отсутствовать (Haskell). Если функция записана в стиле tail recursion, память отжираться не будет.

Александр Гранин

Павел, у SCADA-like и embedded систем (к которым относится софт для самолетов, космических кораблей, критической инфраструктуры) другие принципы построения. Запрет тех или иных конструкций делается не потому, что эти конструкции «плохие», а потому, что их сложно использовать так, чтобы 100% избежать ошибок. Чем меньше опасных конструкций, тем меньше вероятность ошибки. В том же С++ и С, которые могут использоваться в критической инфраструктуре, очень много опасностей. И поскольку языки / компиляторы не помогают программисту писать безопасный код, приходится устанавливать административные правила, снижающие риск непредвиденных ошибок.

Дмитрий Сербин

Yury, императивный стиль.

Максим Корецкий

Михаил, вам кажется. Для ответственной техники введение таких правил вполне ожидаемо. Пруфы: Что там у боинга — не знаю, а вот во внутренний стандарт лаборатории реактивного движения наса рекурсия точно входит.

Юрий Морозов

Михаил, очень смешно. 🙂 Итак, список парадигм, через запятую, опустив ФП, — в студию.

Александр Гранин

А статья, да, слабая.

Юрий Морозов

Дмитрий, не то пальто. Можно писать в стиле, соблюдая принципы.

Михаил Коротков

И что это доказывает? Что рекурсия — зло? Нет. Только то, что в Боинг не используют рекурсию.

Максим Корецкий

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

Юрий Морозов

Я к чему это всё: ФП — чистая афёра, не имеющая под собой вообще никакой твёрдой почвы, все доводы витиеваты и спорны. В чём мы с вами и убедились прямо сейчас.

Дмитрий Сербин

Yury, а если подумать еще раз? Парадигма == стиль написания кода. Две наиболее распространенные парадигмы(стиля): императивная и декларативная. Также в подавляющем большинстве случаев термины «декларативный стиль/парадигма» и «функциональный стиль/парадигма» используют как синонимы. Матчасть бы подтянуть вам.

John Trust

откуда это, можно ссыль?

Юрий Морозов

Дмитрий, уже ли? Вы сами запутались. Здесь: ( «декларативный стиль/парадигма» и «функциональный стиль/парадигма» используют как синонимы), заявляете, что Д и Ф используют как синонимы.

Дмитрий Сербин

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

Михаил Коротков

Юрiй, лично я убедился только в том, что автор статьи считает ФП — это рекурсивные функции без побочных действий. За ФП — не то чтобы будущее, уже и настоящее.

Юрий Морозов

Дмитрий, вы, неуёмный мой друг, с принудительными подтяжками, не поняли вообще ничего в диспуте. Стараетесь выпятиться ярким пятном гения на фоне серой массы остальных юзеров. На вопрос можете ответить: какое ещё программирование бывает, кроме функционального?

Дмитрий Сербин

Yury, уже ответил. Можете прочесть комментарий выше.

Виталий Иванин

Михаил, а не подскажите статью по этой теме? А то я научился прогать на F#, sequence’ы и монады мне очень даже понравились, но чёт пока не знаю где и куда этого ф решетка (ну или хаскель тот же) применять можно. Если нужен мелкий скрипт, то мне проще на питоне написать, так как там с большей вероятностью найдутся нужные либы и туториалы к ним

Андроник Лариков

В мире задач реального мира ФП нигде не используется. Оно не решает никакие задачи. Это просто игра для умов. Но зато как серьезно пишут, ой.

Никита Глухов

Андроник, на самом деле ФП — это академический источник , из которого все полезные плюхи перетекают в индустриальные языки. И это здорово. Я, например, не представляю, как программировать на Scala без лямбд и замыканий или каррирования. Так и должно быть — ООП — это про абстракции и управление сложностью, а ФП — про короткую, валидируемую и ясную запись алгоритма. По отдельности быстро упираешься в различные неудобства и ограниченность, что ООП, что ФП, но вот гибриды потрясающе удобны на практике.

Никита Глухов

Никита Глухов

Виталий, есть продакшн системы на OCaml или хаскель, но их мало, потому что ФП языки-потомки ML требуют (помимо навыка чтения на английском свежей документации, учебников и интуитивного понимания слова «алгоритм») уверенного ориентирования в лямбда-исчислении и теории категорий. А у прикладных программистов на абстрактный матан аллергия — они ж бабки зарабатывают «софтом» и редко разбираются в чём-то, кроме дискретки (графы, реляционные бд и прочие штуки, за которые дают денег «на покушать»). Ну это, в общем-то, нормальная ситуация, программированние хоть и является интеллектуальной деятельностью, но подчиняется законам рынка, где требуются быстрые и дешевые по себестоимости решения, с наценки на которые можно накрутить небывалое количество маржи.

Никита Глухов

Павел, рекурсия — это просто рекурсия, полноправный теоретический подход для конструирования алгоритмов, показывающих хорошую асимптотику, особенно на иерархически структурах данных, либо при решении оптимизационных задач, решаемых динамическим программированием) все её «практические ограничения» связаны с традиционно сложившимся способом организации архитектуры типовых ЭВМ, что функция(метод) гвоздями прибиты к системному стеку, который в разы меньше динамически аллоцируемого хипа. Если задачу удобно решать через рекурсию — да ради бога, просто перепиши алгоритм так, чтобы он промежуточные состояния запихивал во внешний стек. А проблему постоянного перевычисления при рекурсии решают мемоизацией aka золотое правило разработки «посчитал что-то — закешируй».

Вадим Шатов

Андроник, Райфайзен и Тинькофф. Scala с тайплевел стеком. Все приложения в полностью функциональном стиле пишут.

Андроник Лариков

Вадимъ, пример реального приложения.

Вадим Шатов

Андроник, любое приложение которое тебе может прийти в голову. Ты не компетентен

Максим Корецкий

John, гуглил по запросу: NASA JPL Programming standard

Никита Иванов

Юрий, процедурное и объектно-ориентированное, но я не эксперт

Пара Ноик

Андроник, глупости не пишИте. Люксофт, Сбербанк прекрасно используют фп

Виталий Иванин

Никита, «требуют уверенного ориентирования в лямбда исчислении и теории категорий» Это к чему? Лямбда исчисление знаю, практиковал. Это понятно зачем — структуры всякие описывать. А теория категорий зачем? Преобразования типов делать? .

Никита Глухов

Виталий, см. Haskell и его систему типов. Она вся на морфизмах. Удачи в работе с этим языком без знания универского курса теории категорий.

Андроник Лариков

Пара, название конкретной программы дайте. Не надо общих слов.

Андроник Лариков

Вадимъ, ссылку дашь на такое приложение? Только конкретное приложение, а не ваши выдумки.

Пара Ноик

Андроник Лариков

Пара, это просто библиотека для Явы. Можно приложение, которое решает реальные задачи реального мира?

Пара Ноик

Андроник, вы прикидываетесь что-ли? С применением фп написан ряд платформенных микросервисов

Вадим Шатов

Андроник, ссылку дать на внутрибанковский проект ? Хорошая шутка. Я не первый раз вижу высказывания подобно твоему. Пролистай ниже, найди топики с ФП и ты увидишь тоже самое. На самом деле это смешно. Дисциплина ФП сложна и связана с теркатом и теорией типов. Но она очень легка для понимания когда ты уже въехал. Хочешь пруфов? Напиши свое сообщение в scala комьюнити в телеграмме.

Вадим Шатов

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

Илья Крупень

Андроник Лариков

Пара, жду ссылку на проект.

Пара Ноик

Андроник, жди жди)

Вадим Шатов

Андроник, готов показать тебе примеры, скажем, за 800 долларов. Уже много для тебя сделал, за бесплатно.

Андроник Лариков

Вадимъ, я задаю простой вопрос, на который здесь никто не может ответить. В ответ я получаю какие-то общие фразы, рассказы про сложность и тому подобное, вплоть до какой-то там сферической некомпетентности. Нет, ребятки, этим фразами вы можете школьников разве что озадачить. Я, как технический специалист, не вижу в ваших словах никакой семантики. Она стремится к нулю. У нас, к сожалению, программистами сейчас себя называют кто угодно, кто хоть раз «открывал редактор, компилил код». Однако, выполняя механические действия часто забывают о смысле самих действий, над метаинформацией, то, что лежит выше. Я прошу показать проект, который решает проблемы реального мира, а мне в ответ кидают библиотеку ФП для Явы. Гениально.

Андроник Лариков

Пара, что и требовалось доказать — ФП только для интеллектуальных игр. Думать, что ФП как-то где-то что-то делает полезное, это как думать, что олимпиадное программирование будет полезно в промышленном программировании 🙂

Вадим Шатов

Андроник, ты просишь проект на ФП. В Гугле забанили ? Сложно поискать опенсорс? Почему я должен тебе помогать? Хоть 1ну вескую причину мне назови. Все, самые твои гениальные идеи выражаются в ФП. Куча микросервисов написано с применением этой дисциплины. Я сам на работе пишу только в ФП стиле. У нас другой стиль не допускается. Никакого гибридного. Что тебе ещё надо ? Чувак, ты хочешь доказать что я не прав? Мне похеру на тебя. Живи дальше в своем мире.

Пара Ноик

Андроник, дурачком и помрёшь ) ты неконкурентноспособен на рынке, это хорошо

Андроник Лариков

Вадимъ, где семантика? > Почему я должен тебе помогать? Не знаю, я тебя не просил. Ты влился в тред под видом «знающего». Зачем вливаться в тред, если о предмете ничего не знаешь? > Куча микросервисов Снова эта абстракция, не подкрепленная ничем. Ни статистикой, никакими либо другими данными. Вроде же не гуманитарный паблик. Если тебе на меня похеру, зачем ты потратил свое время на меня и еще в таком кол-ве? Ты противоречишь сам себе, причем уже два раза.

Вадим Шатов

Андроник, не трачу чувак 🙂 Отдыхай

Андроник Лариков

Пара, рынок слишком широк и разнообразен, в котором заниматься ФП не так уж и выгодно.

Александр Гранин

Никита, ТК для разработки на Haskell не нужна. Ваш хаскеллист.

Никита Глухов

Александр, ну значит я прост туповат. Когда в свое время брался за хаскель — вообще ничего не понял ни в параметрическом полиморфизме (и всяких экзистенциальных типах), ни в системе типов в целом. Только через курс, прослушав семестр основ теории категорий, я уже смог читать учебники по хаскелю без пустого взгляда на словах врое «моноид», «монада», «стрелки»

Александр Гранин

Никита, ТК будет полезна для понимания концепций «с другого конца». Я профессионально разрабатываю на Haskell 2 года и еще 6 до этого — в качестве хобби. ТК не знаю. Всякие монады, моноиды, функторы и стрелки осознаю через практику и не все сразу.

Никита Глухов

Александр, наверное круто, если у вас так получилось, у меня руки даже до практики не доходят, если для меня фундаментальные основы какой-либо вещи звучат как «заклинания», и чтобы подступиться к этой штуке нужен бекграунд. Но вообще действительно круто, если вы профессионально разрабатываете на Хаскеле на рынке ПО и имеете регулярный доход. В моем городе, к примеру, (да-да, есть удалённая работа, но мне привычнее и спокойнее в офисе сидеть) вакансий на Хаскель вообще нет, и если он и используется где-то, то это в стиле «стихийно получилось». Тип есть архитектор проекта, который по какой-то причине посчитал, что процессинг будет удобно вынести в DLL/SO, собранные с хаскелевского исходного кода, а потом просто линковаться к этой библиотечке в рантайме из обычных языков, на которых, в свою очередь, уже написан всякий «инфраструктурный» код.

Юрий Морозов

Илья, да! Принимается.

Юрий Морозов

Охренеть! Статья из топа — фуфло (это даже я понял), а вот вопрос об определении термина ФП задал жару, да какого горячего! Как я и предположил в начале — никто не смог дать чёткого определения ФП, всё вокруг, да около. Заявляю ещё раз, но уже более осознанно: ФП — фикция, тема для разглагольствования, не более. В деловых проектах главенствует безопасность, надёжность и юзабельность, а ФП — для академических упражнений в программировании математических функций. Применяется там, где не обойтись без строгих math расчётов (физика в играх, например). Ладно, друзья! Всем спасибо, было очень интересно разобраться в этом вопросе.

Александр Гранин

Никита, мне кажется, так у всех. Сначала ты видишь код на Haskell, и он тебе взрывает мозг, а потом ты начинаешь писать маленькие функции вроде fibonacci и факториал, а потом втягиваешься. Первая неделя знакомства с Haskell принесла мне удивительное ощущение. Я не мог дождаться вечера, когда я вернусь после работы и продолжу писать простой код на этом языке. Это было что-то вроде эйфории, и уж точно — любовь с первого взгляда.

Александр Гранин

Андроник, https://bazqux.com/ Коммерческое приложение, написанное на Haskell.

Сергей Лесниченко

Юрий, я не очень понимаю ваши претензии к сообществу. https://ru.wikipedia.org/wiki/Функциональное_программирование — первое предложение. Очевидно, что вы сами могли загуглить эту ссылку до написания своего первого комментария. И да, ФП требует отличных от привычных в си- и паскале- подобных ЯП навыков мышления. ФП предполагает описание алгоритма как выведения набора функций, а не перечисления последовательности операций. Почему, к примеру, в Хаскеле нет понятия цикла. И если предыдущая фраза вам ничего не сказала, то это нормально. Требуется затратить определенные усилия для понимания новой концепции и её практического применения. А рынок — это рынок, там используются самые дешевые из удовлетворительных инструментов, например PHP и JS, по-своему, одни из худших ЯП современности.

Юрий Морозов

Сергей, спасибо! Претензий нет, простите, если так могло показаться. Я пытаюсь разобраться «где заканчивается голова, и начинается хвост».

Сергей Лесниченко

Юрий, да но проблем. Собственно, я тоже не очень понял, почему никто не ответил 🙂

Никита Глухов

Юрий, всё хотел скинуть ссылку на коммерческие приложения того же Хаскеля, но забывал

Юрий Морозов

Друзья! Уже всем спасибо! Всё, что мне было интересно узнать, — я узнал.

Завершить (для себя) этот диспут хочу на юмористической волне. Поскольку в заглавной статье иллюстрация — логотип от Haskel, то так тому и быть: за прошедшие сутки я прочёл больше трёх статей, посвященных ФП, и вот цитата, после которой я решил прекратить изучение вопроса — «Отказ от побочных эффектов и мутабельных состояний позволит вам почувствовать себя выше окружающей толпы даже не отказываясь от пищи животного происхождения и не меняя сексуальную ориентацию!». С новым годом, друзья!

Основные принципы программирования

Программирование – процесс создания программы. Программа – последовательность указаний (команд или описаний и операторов), задающая алгоритм вычислительной машине. Программа указывает, в каком порядке, над какими данными и какие операции должны быть выполнены и в какой форме должен быть выдан результат.

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

Общие принципы, которые следует использовать при разработке программного обеспечения:

· частотный принцип основан на выделении в алгоритмах и в обрабатываемых структурах действий и данных по частоте использования. Для действий, которые часто встречаются при работе программного обеспечения, обеспечиваются условия их быстрого выполнения. К данным, к которым происходит частое обращение, обеспечивается наиболее быстрый доступ. «Частые» операции стараются делать более короткими;

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

· принцип функциональной избирательности является логическим продолжением частотного и модульного принципов и используется при проектировании программного обеспечения, объем которого существенно превосходит имеющийся объем оперативной памяти. В программном обеспечении выделяется некоторая часть важных модулей, которые постоянно должны быть в состоянии готовности для эффективной организации вычислительного процесса. Эту часть программного обеспечения называют ядром или монитором. При формировании состава монитора требуется удовлетворить двум противоречивым требованиям. В состав монитора, помимо чисто управляющих модулей, должны войти наиболее часто используемые модули. Программы, входящие в состав монитора, постоянно хранятся в оперативной памяти. Остальные части программного обеспечения постоянно хранятся во внешних запоминающих устройствах и загружаются в оперативную память только по вызову, перекрывая друг друга при необходимости;

· принцип генерируемости определяет такой способ исходного представления программного обеспечения, который бы позволял осуществлять настройку на конкретную конфигурацию технических средств, круг решаемых проблем, условия работы пользователя;

· принцип функциональной избыточности учитывает возможность проведения одной и той же работы (функции) различными средствами. Особенно важен учет этого принципа при разработке пользовательского интерфейса для выдачи данных из-за психологических различий в восприятии информации;

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

· общесистемные принципы рекомендуется применять следующие общесистемные принципы:

· принцип включения предусматривает, что требования к созданию, функционированию и развитию программного обеспечения определяются со стороны более сложной, включающей его в себя системы;

· принцип системного единства состоит в том, что на всех стадиях создания, функционирования и развития программного обеспечения его целостность будет обеспечиваться связями между подсистемами, а также функционированием подсистемы управления;

· принцип развития предусматривает в программном обеспечении возможность его наращивания и совершенствования компонентов и связей между ними;

· принцип комплексности заключается в том, что программное обеспечение обеспечивает связность обработки информации, как отдельных элементов, так и для всего объема данных в целом на всех стадиях обработки;

· принцип информационного единства, т.е. во всех подсистемах, средствах обеспечения и компонентах программного обеспечения используются единые термины, символы, условные обо­значения и способы представления;

· принцип совместимости язык, символы, коды и средства обеспечения программного обеспечения согласованы, обеспечивают совместное функционирование всех его подсистем и сохраняют открытой структуру системы в целом;

· принцип инвариантности предопределяет, что подсистемы и компоненты программного обеспечения инвариантны к обрабатываемой информации, т.е. являются универсальными или типовыми.

Программирование включает в себя анализ требований к программе и все стадии ее разработки и реализации: выбор алгоритма, структуры данных и системы программирования; написание (кодирование) программы и подготовка данных; отладка и тестирование программы; разработка документации.

Элементы функционального программирования. Принципы функционального программирования: почему это важно

Язык функционального программирования

В качестве основных свойств функциональных языков программирования обычно рассматриваются [кем? ] следующие:

Программы на функциональных языках обычно намного короче и проще, чем те же самые программы на императивных языках.
Пример (быстрая сортировка Хоара на абстрактном функциональном языке) :

QuickSort () =
quickSort () = quickSort (n | n t, n h)

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

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

В функциональных языках (равно как и вообще в языках программирования и математике) функции могут быть переданы другим функциям в качестве аргумента или возвращены в качестве результата. Функции, принимающие функциональные аргументы, называются функциями высших порядков или функционалами.

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

В традиционных языках программирования (например, C++) вызов функции приводит к вычислению всех аргументов. Этот метод вызова функции называется вызов-по-значению. Если какой-либо аргумент не использовался в функции, то результат вычислений пропадает, следовательно, вычисления были произведены впустую. В каком-то смысле противоположностью вызова-по-значению является вызов-по-необходимости (ленивые вычисления). В этом случае аргумент вычисляется, только если он нужен для вычисления результата.

Некоторые языки функционального программирования

Классификация функциональных языков

В качестве примера чистого функционального языка можно привести Haskell . Однако большинство функциональных языков являются гибридными и содержат свойства как функциональных, так и императивных языков. Яркие примеры — языки Scala и Nemerle. В них органично сочетаются характеристики как объектно-ориентированных языков, так и функциональных. Реализована хвостовая рекурсия и её оптимизация, функция является полноправным объектом, то есть может быть сохранена в переменной, передана в качестве аргумента в другую функцию или возвращена из функции.

Также функциональные языки делят на строгие и нестрогие . К нестрогим языкам относят те, которые поддерживают отложенные вычисления (F#), то есть аргументы функции вычисляются только тогда, когда они действительно понадобятся при вычислении функции. Ярким примером нестрогого языка является Haskell. В качестве примера строгого языка можно привести Standard ML .

Некоторые функциональные языки реализованы поверх платформообразующих виртуальных машин (JVM, .NET), то есть приложения на этих языках могут работать в среде времени исполнения (JRE, CLR) и использовать встроенные классы. К ним относятся Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

Ссылки

  • https://fprog.ru/ — Журнал «Практика функционального программирования»
  • https://www.intuit.ru/department/pl/funcpl/1/ — Основы функционального программирования. Л. В. Городняя
  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года;
  • https://alexott.net/ru/fp/books/ — Обзор литературы о функциональном программировании . Рассматриваются книги как на русском, так и на английском языке.

Wikimedia Foundation . 2010 .

Смотреть что такое «Язык функционального программирования» в других словарях:

язык прграммирования Лисп — Язык функционального программирования. Тематики информационные технологии в целом EN Lisp … Справочник технического переводчика

Универсальный язык программирования высокого уровня. Язык Лисп: относится к декларативным языкам функционального типа; предназначен для обработки символьных данных, представленных в виде списков. Основой языка являются функции и рекурсивные… … Финансовый словарь

У этого термина существуют и другие значения, см. Alice. Alice Семантика: функциональный Тип исполнения: компиляция в байткод для виртуальной машины Появился в: 2002 … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Oz Семантика: функциональный, процедурный, декларативный, объектно ориентированный, вычисления с ограничениями, Н модели, параллельные вычисления Тип исполнения: компилируемый Появился в: 1991 Автор(ы): Gert Smolka his students Релиз … Википедия

AWL (Alternative Web Language) Класс языка: мультипарадигмальный: функциональный, процедурный, объектно ориентированный Тип исполнения: интерпретируемый Появился в: 2005 г. Типизация данных: динамическая … Википедия

У этого термина существуют и другие значения, см. Леда (значения). Леда (Leda) мультипарадигмальный язык программирования, спроектированный Тимоти Баддом. Язык Leda исходно создавался с целью совмещения императивного программирования, объектно… … Википедия

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

В языках функционального программирования основным конструктивным элементом является математическое понятие функции. Существует различия в понимании функции в математике и функции в программировании, в следствии чего нельзя отнести Си подобные… … Википедия

Python был задуман в 1980 х годах, а его создание началось в декабре 1989 года Гвидо ван Россумом в составе центра математики и информатики в Нидерландах. Язык Python был задуман как потомок языка программирования ABC, способный к обработке… … Википедия

Функциональное программирование объединяет разные подходы к определению процессов вычисления на основе достаточно строгих абстрактных понятий и методов символьной обработки данных.

Особенностью языков функционального программирования является то, что тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. Основные свойства функциональных языков программирования: краткость, простота, строгая типизация, модульность, наличие отложенных (ленивых) вычислений.

К функциональным языкам программирования относят: Lisp, Miranda, Gofel, ML, Standard ML, Objective CAML, F#, Scala, Пифагор и др.

Процедурные языки программирования

Процедурный язык программирования предоставляет возможность программисту определять каждый шаг в процессе решения задачи. Особенность таких языков программирования состоит в том, что задачи разбиваются на шаги и решаются шаг за шагом. Используя процедурный язык, программист определяет языковые конструкции для выполнения последовательности алгоритмических шагов.

Процедурные языки программирования: Ada, Basic, Си, КОБОЛ, Pascal, ПЛ/1, Рапира и др.

Стековые языки программирования

Стековый язык программирования − это язык программирования, в котором для передачи параметров используется машинная модель стека. Стековые языки программирования: Forth, PostScript, Java, C# и др. При использовании стека, в качестве основного канала передачи параметров между словами, элементы языка, естественным образом, образуют фразы (последовательное сцепление). Это свойство сближает данные языки с естественными языками.

Аспектно-ориентированные языки программирования 5) Декларативные языки программирования 6) Динамические языки программирования 7) Учебные языки программирования 8) Языки описания интерфейсов 9) Языки прототипного программирования 10) Объектно-ориентированные языки программирования 11) Логические языки программирования 12) Сценарные языки программирования 13) Эзотерические языки программирования

Стандартизация языков программирования. Парадигма программирования

Концепция языка программирования неотрывно связана с его реализацией. Для того чтобы компиляция одной и той же программы различными компиляторами всегда давала одинаковый результат, разрабатываются стандарты языков программирования. Организации, занимающиеся вопросами стандартизации: Американский национальный институт стандартов ANSI, Институт инженеров по электротехнике и электронике IEEE, Организация международных стандартов ISO.

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

Парадигма – набор теорий, стандартов и методов, которые совместно представляют собой способ организации научного знания, – иными словами, способ видения мира. По аналогии с этим принято считать, что парадигма в программировании – способ концептуализации, который определяет, как следует проводить вычисления, и как работа, выполняемая компьютером, должна быть структурирована и организована.

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

C и Pascal являются примерами языков, предназначенных для директивного программирования, когда разработчик программы использует процессно-ориентированная модель, то есть пытается создать код, должным образом воздействующий на данные. Активным началом при этом подходе считается программа (код), которая должна выполнить все необходимые для достижения нужного результата действия над пассивными данными.

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

Программирование – процесс создания компьютерных программ. В более широком смысле: спектр деят-сти, связ-ый с созданием и поддержанием в раб. состоянии программ — ПО ЭВМ.

Технология программирования — совокупность методов и средств, используемых в процессе разработки программного обеспечения.

Технология программир-я представляет собой набор технологических инструкций, включающих:

· указание последоват-сти выполнения технологич-х операций;

· перечисление условий, при кот-х выполняется та или иная операция;

· описания самих операций, где для каждой операции определены исходные данные, результаты, а также инструкции, нормативы, стандарты, критерии и т. п.

Современная технология программирования — компонентный подход , который предполагает построение программного обеспечения из отдельных компонентов — физически отдельно существующих частей программного обеспечения, которые взаимодействуют между собой через стандартизованные двоичные интерфейсы. В настоящее время критериями качества программного продукта принято считать:− функциональность ; − надежность ;− легкость применения ;− эффективность (отношение уровня услуг, предоставл-х программным продуктом пользов-лю при заданных условиях, к объему используемых ресурсов);− сопровождаемость (характер-ки программ-го продукта, которые позволяют минимизир-ть усилия по внесению изменений для устранения в нем ошибок и по его модификации в соотв-вии с изменяющ-ся потребностями пользов-лей);− мобильность (способность ПС быть перенесенным из одной среды в другую, в частности, с одной ЭВМ на др.).

Важным этапом создания прогр-го продукта явл. тестирование и отладка.

Отладка − это деятельность, направленная на обнаружение и исправление ошибок в программном продукте с использованием процессов выполнения его программ.

Тестирование − это процесс выполнения его программ на некотором наборе данных, для которого заранее известен результат применения или известны правила поведения этих программ.

Существуют следующие методы тестирования ПС:

1) Статическое тестирование – ручная проверка программы за столом.

2) Детерминированное тестир-е – при разл-х комбинациях исх-х данных.

3) Стохастическое – исх. данные выбир-ся произвольно, на выходе определяется качеств-е совпадение результатов или примерная оценка.

Стиль программирования — набор приемов или методов программирования, которые используют программисты, чтобы получить правильные, эффективные, удобные для применения и легкочитаемые программы.

Существует несколько стилей программирования:

  1. Процедурное программирование – это программирование, при котором программа представляет собой последовательность операторов. Используется в языках высокого уровня Basic, Fortran и др.
  2. Функциональное программирование – это программирование, при котором программа представляет собой последовательность вызовов функций. Используется в языках Lisp и др.
  3. Логическоепрограммирование – это программирование, при котором программа представляет собой совокупность определения соотношений между объектами. Используется в языках Prolog и др.

Объектно-ориентированноепрограммирование – это программирование, при котором основой программы является объект представляющий собой совокупность данных и правил их преобразования. Используется в языках Turbo-Pascal, C++ и др.

Программы на традиционных языках программирования, таких как Си, Паскаль, Java и т.п. состоят их последовательности модификаций значений некоторого набора переменных, который называется состоянием . Если не рассматривать операции ввода-вывода, а также не учитывать того факта, что программа может работать непрерывно (т.е. без остановок, как в случае серверных программ), можно сделать следующую абстракцию. До начала выполнения программы состояние имеет некоторое начальное значение σ0 , в котором представлены входные значения программы. После завершения программы состояние имеет новое значение σ0 , включающее в себя то, что можно рассматривать как «результат» работы программы. Во время исполнения каждая команда изменяет состояние; следовательно, состояние проходит через некоторую конечную последовательность значений:

σ = σ0 → σ1 → σ2 → · · · → σn = σ0

Состояние модифицируется с помощью команд присваивания , записываемых в виде v=E или v:=E, где v — переменная, а E — некоторое выражение. Эти команды следуют одна за другой; операторы, такие как if и while, позволяют изменить порядок выполнения этих команд в зависимости от текущего значения состояния. Такой стиль программирования называютимперативным илипроцедурным .

Функциональное программирование представляет парадигму, в корне отличную от представленной выше модели. Функциональная программа представляет собой некоторое выражение (в математическом смысле); выполнение программы означает вычисление значения этого выражения.1 С учетом приведенных выше обозначений, считая что результат работы

1 Употребление термина «вычисление» не означает, что программа может оперировать только с числами; результатом вычисления могут оказаться строки, списки и вообще, любые допустимые в языке структуры данных.

императивной программы полностью и однозначно определен ее входом, можно сказать, что финальное состояние (или любое промежуточное) представляет собой некоторую функцию (в математическом смысле) от начального состояния, т.е. σ0 = f(σ). В функционально программировании используется именно эта точка зрения: программа представляет собой выражение, соответствующее функции f. Функциональные языки программирования поддерживают построение таких выражений, предоставляю широкий выбор соответствующих языковых конструкций.

При сравнении функционального и императивного подхода к программированию можно заметить следующие свойства функциональных программ:

Функциональные программы не используют переменные в том смысле, в котором это слово употребляется в императивном программировании. В частности в функциональных программах не используется оператор присваивания.

Как следствие из предыдущего пункта, в функциональных программах нет циклов.

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

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

Вместо циклов функциональные программы широко используют рекурсивные функции.

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

Прежде всего, императивный стиль в программировании не является жестко заданной необходимостью. Многие характеристики императивных языков программирования являются результатом абстрагирования от низкоуровневых деталей реализации компьютера, от машинных кодов к языкам ассемблера, а затем к языкам типа Фортрана и т.д. Однако нет причин полагать, что такие языки отражают наиболее естественный для

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

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

Например, рассмотрим следующую программу на языке Haskell:

factorial n = if n == 0 then 1 else n * factorial (n — 1)

Практически сразу видно, что эта программа соответствует следующей частичной функции:

(Здесь символ означает неопределенность функции, поскольку при отрицательных значениях аргумента программа не завершается.) Однако для программы на языке Си это соответствие не очевидно:

int x = 1; while (n > 0)

x = x * n; n = n — 1;

Следует также сделать замечание относительно употребления термина «функция» в таких языках как Си, Java и т.п. В математическом смысле «функции» языка Си не являются функциями, поскольку:

Их значение может зависеть не только от аргументов;

Результатом их выполнения могут быть разнообразные побочные эффекты (например, изменение значений глобальных переменных)

Два вызова одной и той же функции с одними и теми же аргументами могут привести к различным результатам.

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

2 Основы лямбда-исчисления

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

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

В настоящее время лямбда-исчисление является основной из таких формализаций, применяемой в исследованиях связанных с языками программирования. Связано это, вероятно, со следующими факторами:

Это единственная формализация, которая, хотя и с некоторыми неудобствами, действительно может быть непосредственно использована для написания программ.

Лямбда-исчисление дает простую и естественную модель для таких важных понятий, как рекурсия и вложенные среды.

Большинство конструкций традиционных языков программирования может быть более или менее непосредственно отображено в конструкции лямбда-исчисления.

Функциональные языки являются в основном удобной формой синтаксической записи для конструкций различных вариантов лямбдаисчисления. Некоторые современные языки (Haskell, Clean) имеют

100% соответствие своей семантики с семантикой подразумеваемых конструкций лямбда-исчисления.

В математике, когда необходимо говорить о какой-либо функции, принято давать этой функции некоторое имя и впоследствии использовать его, как, например, в следующем утверждении:

Пусть f: R → R определяется следующим выражением:

Тогда f0 (x) не интегрируема на интервале .

Многие языки программирования также допускают определение функций только с присваиванием им некоторых имен. Например, в языке Си функция всегда должна иметь имя. Это кажется естественным, однако поскольку в функциональном программировании функции используются повсеместно, такой подход может привести к серьезным затруднениям. Представьте себе, что мы должны всегда оперировать с арифметическими выражениями в подобном стиле:

Пусть x = 2 и y = 4. Тогда xx = y.

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

где E — некоторое выражение, возможно, использующее переменную x.

Пример. λx.x2 представляет собой функцию, возводящую свой аргумент в квадрат.

Использование лямбда-нотации позволяет четко разделить случаи, когда под выражением вида f(x) мы понимаем саму функцию f и ее значение в точке x. Кроме того, лямбда-нотация позволяет формализовать практически все виды математической нотации. Если начать с констант и переменных и строить выражения только с помощью лямбда-выраже- ний и применений функции к аргументам, то можно представить очень сложные математические выражения.

Применение функции f к аргументу x мы будем обозначать как f x, т.е., в отличие от того, как это принято в математике, не будем использовать скобки2 . По причинам, которые станут ясны позднее, будем считать, что применение функции к аргументу ассоциативно влево, т.е. f x y

2 Заметим, что и в математике такие выражения, как sin x записываются без скобок.

означает (f(x))(y). В качестве сокращения для выражений вида λx.λy.E будем использовать запись λx y.E (аналогично для большего числа аргументов). Также будем считать, что «область действия» лямбда-выра- жения простирается вправо насколько возможно, т.е., например, λx.x y означает λx.(x y), а не (λx.x)y.

На первый взгляд кажется, что нам необходимо ввести специальное обозначение для функций нескольких аргументов. Однако существует операция каррирования 3 , позволяющая записать такие функции в обычной лямбда-нотации. Идея заключается в том, чтобы использовать выражения вида λx y.x + y. Такое выражение можно рассматривать как функцию R → (R → R), т.е. если его применить к одному аргументу, результатом будет функция, которая затем принимает другой аргумент. Таким образом:

(λx y.x + y) 1 2 = (λy.1 + y) 2 = 1 + 2.

Переменные в лямбда-выражениях могут бытьсвободными исвязанными . В выражении вида x2 + x переменная x является свободной; его значение зависит от значения переменной x и в общем случае ее нельзя

вать обозначение j, значение выражения не изменится.

Следует понимать, что в каком-либо подвыражении переменная может быть свободной (как в выражении под интегралом), однако во всем выражении она связана какой-либо операцией связывания переменной , такой как операция суммирования. Та часть выражения, которая находится «внутри» операции связывания, называется областью видимости переменной.

В лямбда исчислении выражения λx.E[x] и λy.E[y] считаются эквивалентными (это называется α-эквивалентностью, и процесс преобразования между такими парами называют α-преобразованием). Разумеется, необходимо наложить условие, что y не является свободной переменной в E[x].

3 от фамилии известного логика Хаскелла Карри, в честь которого назван язык программирования Haskell

3 Лямбда-исчисление как формальная система

Лямбда-исчисление основано на формальной нотации лямбда-терма, составляемого из переменных и некоторого фиксированного набора констант с использованием операции применения функции и лямбда-абстра- гирования. Сказанное означает, что все лямбда-выражения можно разделить на четыре категории:

1. Переменные: обозначаются произвольными строками, составленными из букв и цифр.

2. Константы: также обозначаются строками; отличие от переменных будем определять из контекста.

3. Комбинации: , т.е. применения функции S к аргументу T ; и S и T могут быть произвольными лямбда-термами. Комбинация записывается как S T .

4. Абстракции произвольного лямбда-терма S по переменной x, обозначаемые как λx.S.

Таким образом, лямбда-терм определяется рекурсивно и его грамматику можно определить в виде следующей формы Бэкуса-Наура:

Exp = Var| Const| Exp Exp| λ Var . Exp

В соответствие с этой грамматикой лямбда-термы представляются в виде синтаксических деревьев, а не в виде последовательности символов. Отсюда следует, что соглашения об ассоциативности операции применения функции, эквивалентность выражений вида λx y.S и λx.λy.S, неоднозначность в именах констант и переменных проистекают только из необходимости представления лямбда-термов в удобном человеку виде, и не являются частью формальной системы.

3.1 Свободные и связанные переменные

В данном разделе мы формализуем данное ранее интуитивное представление о свободных и связанных переменных. Множество свободных

переменных F V (S) лямбда-терма S можно определить рекурсивно следующим образом:

Аналогично множество связанных переменных BV (S) определяется следующими формулами:

BV (S T) = BV (S) BV (T)

Здесь предполагается, что c — некоторая константа.

Пример. Для терма S = (λx y.x) (λx.z x) можно показать, что F V (S) = и

Интуитивно ясно, что применение терма λx.S как функции к аргументу T дает в результате терм S, в котором все свободные вхождения переменной x заменены на T . Как ни странно, формализовать это интуитивное представление оказывается нелегко.

Будем обозначать операцию подстановки терма S вместо переменной x в другом терме T как T . Также, как и в определение свободных и связанных переменных, правила подстановки также можно определить рекурсивно. Трудность состоит в том, что необходимо наложить дополнительные ограничения, позволяющие избегать конфликта в именах переменных.

Лямбда-исчисление основано на трех операциях конверсии, которые позволяют переходить от одного терма к другому, эквивалентному ему. По сложившейся традиции эти конверсии обозначают греческими буквами α, β и η. Они определяются следующим образом:

α-конверсия: λx.S −→ λy.S при условии, что y / F V (S).

Например, λu.u v −→ λw.w u.

β-конверсия: (λx.S) T −→ S.

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

3.4 Равенство лямбда-термов

Используя введенные правила конверсии, можно формально определить понятие равенства лямбда-термов. Два терма равны, если от одного из них можно перейти к другому с помощью конечной последовательности конверсий. Определим понятие равенства следующими выражениями, в которых горизонтальные линии следует понимать как «если утверждение над чертой выполняется, то выполняется и утверждение

Следует отличать понятие равенства, определяемое этими формулами, от понятия синтаксической эквивалентности, которую мы будем обозначать специальным символом ≡. Например, λx.x 6≡λy.y, но λx.x = λy.y. Часто можно рассматривать синтаксическую эквивалентность термов с точностью до α-конверсий. Такую эквивалентность будем обозначать символом ≡α . Это отношение определяется так же, как равенство лямбда-термов, за тем исключением, что из всех конверсий допустимы только α-конверсии. Таким образом, λx.x ≡α λy.y.

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

— ООП не сможет больше спасать нас от «Облачных монстров».

Примечание переводчика: Есть два понятия — параллельность (выполнение одновременно, независимо) и конкурентность (выполнение по шагам, поочерёдно, но одновременно несколько задач) и как всегда, мне пришлось поломать голову подобрая правильные термины.

Некоторые слова или термины я буду дублировать в скобках в оригинале, для того, чтобы искать по англоязычным терминам дополнительную информацию, которой будет в разы больше.

Возможно вы уже слышали такое выражение, вроде: “Clojure”, “Scala”, “Erlang” или даже “Java теперь имеет лямбды”. И вы имеете хоть и отдалённое представление о «Функциональном программировании». Если вы участник какого-либа программисткого сообщества, тогда эта тема могла уже вами обсуждаться.

Если вы поищите в Google по словосочетанию «Функциональное программирование», вы не увидите что-то нового. Второй язык из созданных ранее уже охватывает эту тему, он был создан в 50-ых и называется Lisp. Тогда, какого чёрта, эта тема стала популярна только сейчас? Всего то 60 лет спустя?

В начале, компьютеры были очень медленными

Верите вы этому или нет, но компьютеры были нааамного медленнее чем DOM. Нет, действительно. И в то-же время были 2 основные идеи в соглашении по дизайну и реализации языков программирования:

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

Функции являются абстракциями , в которых детали реализации некоторого действия скрываются за отдельным именем. Хорошо написанный набор функций позволяет использовать их много раз. Стандартная библиотека Python содержит множество готовых и отлаженных функций, многие из которых достаточно универсальны, чтобы работать с широким спектром входных данных. Даже если некоторый участок кода не используется несколько раз, но по входным и выходным данным он достаточно автономен, его смело можно выделить в отдельную функцию.

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

Далее будут подробно рассмотрены описание и использование функций в Python , рекурсия , передача и возврат функций в качестве параметров, обработка последовательностей и итераторы , а также такое понятие как генератор . Будет продемонстрировано, что в Python функции являются объектами (и, значит, могут быть переданы в качестве параметров и возвращены в результате выполнения функций). Кроме того, речь пойдет о том, как можно реализовать некоторые механизмы функционального программирования, не имеющие в Python прямой синтаксической поддержки, но широко распространенные в языках функционального программирования.

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

Функциональное программирование — это стиль программирования, использующий только композиции функций . Другими словами, это программирование в выражениях, а не в императивных командах.

Как отмечает Дэвид Мертц (David Mertz) в своей статье о функциональном программировании на Python , «функциональное программирование — программирование на функциональных языках ( LISP , ML, OCAML, Haskell, . )», основными атрибутами которых являются:

  • «Наличие функций первого класса» (функции наравне с другими объектами можно передавать внутрь функций).
  • Рекурсия является основной управляющей структурой в программе.
  • Обработка списков (последовательностей).
  • Запрещение побочных эффектов у функций, что в первую очередь означает отсутствие присваивания (в «чистых» функциональных языках)
  • Запрещение операторов, основной упор делается на выражения. Вместо операторов вся программа в идеале — одно выражение с сопутствующими определениями.
  • Ключевой вопрос: что нужно вычислить, а не как .
  • Использование функций более высоких порядков (функции над функциями над функциями).

Функциональная программа

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

Программы в функциональном стиле конструируются как композиция функций. При этом функции понимаются почти так же, как и в математике: они отображают одни объекты в другие. В программировании «чистые» функции — идеал, не всегда достижимый на практике. Практически полезные функции обычно имеют побочный эффект : сохраняют состояние между вызовами или меняют состояние других объектов. Например, без побочных эффектов невозможно представить себе функции ввода-вывода. Собственно, такие функции ради этих «эффектов» и используются. Кроме того, математические функции легко работают с объектами, требующими бесконечного объема информации (например, вещественные числа). В общем случае компьютерная

Добавить комментарий