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

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

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

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

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

Сейчас программисты чаще всего работают с ООП — объектно-ориентированным программированием. Функциональный подход используют реже: он сложнее и применим не ко всем задачам. Но сейчас его популярность растет, и тому есть причины: чистота кода, надежность программ, высокие возможности для оптимизации.

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

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

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

Условно можно выделить две группы. Первая — языки, жестко ориентированные на функциональное программирование. Вторая — так называемые мультипарадигменные языки, то есть такие, на которых можно писать по-разному. В том числе в функциональном стиле.

К функциональным языкам относятся Haskell, F#, OCaml, ELM, серия языков Lisp, а также Erlang и его потомок Elixir. Иногда сюда же относят Scala и Nemerle, хотя эти языки дают возможность программировать и в функциональном, и в императивном стилях. Они старые и сейчас применяются не так часто, как большинство современных.

Еще к этой группе относится несколько узкоспециализированных языков: язык вычислительной платформы Wolfram, языки J и K для финансового анализа, язык R для статистических целей. На функциональном языке APL базируется язык научной математической среды MATLAB. Также сюда можно отнести языки, которые используются при работе с электронными таблицами, а еще, частично, SQL — язык запросов для работы с базами данных.

К мультипарадигменным языкам, на которых можно писать в функциональном стиле, относятся уже упомянутые Scala и Nemerle, а также Go, JavaScript и некоторые его фреймворки. В меньшей степени сюда же можно отнести Python, Ruby, PHP и C++, а также Java: они больше ориентированы на ООП, но в них есть и функциональные элементы.

Разница между функциональным и императивным подходом

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

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

В чем разница с ООП

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

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

Сложность изучения функциональных языков

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

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

Особенности функционального подхода

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

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

«Чистые» функции. Чистые функции — это такие, которые удовлетворяют двум условиям:

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

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

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

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

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

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

Относительная прозрачность. Еще одно требование к функциям в функциональном программировании — относительная прозрачность. Это понятие может быть сложным для понимания, но мы постараемся его объяснить. Относительная прозрачность означает, что выражение, которое возвращает функция, можно заменить значением — и от этого ничего не изменится. То есть, если функция, например, складывает два числа 3 и 5, то она вернет сумму 3 + 5. Теоретически вместо этой функции в выражение можно подставить число 8, и от этого программа не изменится — она будет работать так же.

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

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

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

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

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

  • в λ-исчислении есть две основных операции — аппликация и абстракция. Первое — это, по сути, вызов функции к заданному значению. Второе — построение функции по имеющимся выражениям;
  • все функции могут быть анонимными и складываться только из списка аргументов. Анонимные функции — это такие, у которых нет уникального имени, а объявляются они в месте выполнения;
  • при вызове функции с несколькими аргументами происходит ее каррирование — преобразование в несколько функций, в каждой из которых один аргумент. То есть, функция вида f(a, b, c) превратится в набор функций f(a)(b)(c). Результатом f(a) будет функция, которая тут же применится к аргументу b. И так далее. Это рекурсия — та, о которой мы говорили выше.

Преимущества функциональных языков

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

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

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

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

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

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

Недостатки функциональных языков

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

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

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

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

Сначала вам понадобится познакомиться с основами парадигмы и с теорией. Можно скомбинировать это с началом изучения функциональных языков, чтобы сразу «пощупать» подход на практике. Но помните, что многие решения сначала могут показаться вам неочевидными — к особым принципам нужно привыкнуть. Тем не менее, функциональное программирование – мощный и интересный инструмент, и изучить его вполне реально даже новичку.

Другие термины на букву «Ф»

Файервол
Фреймворк
Фриланс

Все термины

(рейтинг: 5, голосов: 2)
Добавить комментарий