Привет, Хабр! Меня зовут Артём Корсаков. Я руководитель группы серверной разработки в компании "Криптонит". Пишу на Scala и веду проект scalabook.ru. В этой статье мы разберём основы функционального программирования (ФП) на примерах и с поправкой на суровую действительность.
Многие слышали о преимуществах ФП, но пока не понимают, как применить эти концепции на практике. Теория монад и функторов и вовсе отпугивает, кажется чем-то заумным и непонятно где применимым. Поэтому мы отбросим сложную теорию и сосредоточимся на основных моментах, которые вы сразу сможете начать использовать в своём коде.
Одна из особенностей работы в реальных проектах состоит в том, что бизнесу нет дела до красоты кода. Большинству заказчиков нужен быстрый результат, возможность показать прототип раньше других или первыми урвать выгодный контракт. Поэтому для бизнес-проектов ближнего прицела типичны одни и те же проблемы, связанные с текучкой кадров и нечитаемым кодом на костылях. Но для запуска прототипа и этого вполне достаточно!
Другое дело — долгосрочные проекты, в которых сразу закладывается требование многолетней поддержки. Тогда можно с самого начала разрабатывать так, чтобы:
код был читаемым;
он не содержал критических ошибок;
его было легко поддерживать, расширять и тестировать;
чтобы другие разработчики даже спустя годы могли понять, что именно делает код.
И, правда, как мы раньше об этом не догадались за почти двести лет программирования, начиная с ткацкого станка Жаккара и трудов Ады Лавлейс? Покажите мне хоть одного пользователя, который ни разу в жизни не прокричал: «Что же эти разрабы не могут сделать приложение без багов?!». Сам так не раз восклицал (по отношению к чужому коду, конечно, 😉).
Именно здесь функциональное программирование показывает себя с самой лучшей стороны.
Перефразируя изречение американского программиста Ярона Минского:
«Сила функционального программирования — в моделировании предметной области таким образом, чтобы ошибочные состояния были исключены самим подходом и системой типов».
Одним из важных отличий ФП является использование так называемых "чистых функций". Рассмотрим их подробнее.
Чистые функции (Pure Functions) как фундамент предсказуемости
Функция, которая для одних и тех же входных данных всегда возвращает один и тот же результат и не имеет побочных эффектов (не изменяет внешнее состояние, не работает с вводом/выводом) называется чистой.
Допустим, нам необходимо построить диалог с пользователем и мы привычно написали в императивном стиле:
import scala.io.StdIn.readLine
@main def imperative(): Unit =
println("=== Анализатор языков программирования ===")
val language = readLine("Введите язык программирования: ").trim.toLowerCase
if language == "haskell" then
println(s"$language - это функциональный язык программирования!")
return
if language == "scala" then
println(
s"$language - мультипарадигменный язык с поддержкой функционального программирования"
)
val answer = readLine("А ты используешь принципы ФП в своем коде? (y/n)")
if answer == "y" then
println("Отлично!")
return
println(
"Тогда ты используешь императивный стиль программирования, как, например, вот тут: https://github.com/vesoft-inc/nebula-spark-connector/tree/master"
)
return
if language == "java" then
println(s"$language - в основном императивный/ООП язык")
return
println(s"$language - тип языка не определен или это новый язык")
end imperative
Чтобы убедиться в работоспособности кода нам нужно 5 раз запустить программу вручную для следующих вариантов: «haskell», «scala» -> y, «scala» -> n, «java» и любой другой язык программирования, — например, «python».
А что, если в ответе нужно учесть свыше сотни языков?!
В этом случае разработчик отказывается тестировать свой код и делегирует эту задачу отделу QA, в котором где-то на 30-м варианте выгорают даже самые стойкие инженеры по тестированию. Дальше код уходит клиенту, который пытается ввести свой любимый язык программирования (допустим, Piet) и получает исключение: «я не знаю такого языка». Именно тут появляется крик души пользователей: «Что же эти разрабы не могут сделать приложение без багов?!».
Так как же сделать код более предсказуемым и облегчить его поддержку?
Для начала вспомним, что такое «return». Return
, throw exceptions
и goto
— это всё одно и то же: прерывание выполнения программы. Прерывание! Когда мы идём, идём по дороге, а затем — БАХ — и упали в открытый канализационный люк! Если оператор goto
заслужил-таки негативную репутацию и сейчас практически не используется, то его братья return
и exceptions
встречаются повсеместно. Скорее всего, потому, что в этих случаях прерывание не так заметно.
Суть функционального программирования состоит в создании композиции функций. Мы всегда что-то получаем на выходе. У нас есть функция из «A» в «B», и мы всегда обязаны обработать «B», чтобы пойти дальше. Мы исключаем прерывание как управление порядком выполнения (control flow) и всегда идём до конца, что заставляет нас продумывать все варианты завершения. Мы не проваливаемся в канализационный люк, а видим разветвления и делаем выбор!
Давайте избавимся от прерывания и попробуем выделить чистые функции:
@main def functional(): Unit =
def describeLanguage(language: String): String = language match
case "haskell" => s"$language - это функциональный язык программирования!"
case "scala" =>
s"$language - мультипарадигменный язык с поддержкой функционального программирования"
case "java" => s"$language - в основном императивный/ООП язык"
case _ => s"$language - тип языка не определен или это новый язык"
def describeFp(answer: Boolean): String =
if answer then "Отлично!"
else
"Тогда ты используешь императивный стиль программирования, " +
"как, например, вот тут: https://github.com/vesoft-inc/nebula-spark-connector/tree/master"
println("=== Анализатор языков программирования ===")
val language = readLine("Введите язык программирования: ").trim.toLowerCase
println(describeLanguage(language))
if language == "scala" then
val answer = readLine("А ты используешь принципы ФП в своем коде? (y/n)")
println(describeFp(answer == "y"))
end functional
Уже полегче, потому что на чистые функции разработчик может с лёгкостью написать юнит-тесты, а может даже делегировать эту работу большой языковой модели (ChatGPT, DeepSeek, GigaChat — нужное подчеркнуть). Любая LLM со скоростью света нагенерирует все 200+ вариантов для describeLanguage
, а также два варианта для describeFp
.
QA останется только вручную проверить три вариант��: "Не Scala", "Scala + Да", "Scala + Нет".
И даже это не конец: можно все, что внутри метода @main
сделать чистой функцией!
Представьте выражение лица QA, которому нужно проверить только один вариант: что программа запускается!!!

Функциональное программирование — это не хардкорная математика, а практический подход к созданию надёжного и предсказуемого кода. Начните с малого: откажитесь от лишних return
, сведите к минимуму исключения и дробите большие функции на чистые, переиспользуемые части. Да, сначала это будет непривычно — хорошее проектирование всегда требует усилий. Но скоро вы заметите, что стали мыслить иначе: вместо последовательности действий вы начнёте видеть элегантные цепочки преобразований данных.
Чтобы углубиться в тему, исследуйте нашу базу знаний по Scala — Scalabook. Мы постоянно её пополняем и ждём вашей обратной связи!
А впереди — много практики! В следующих статьях мы разберём:
Как определять структуры данных?
Как взаимодействовать с конфигами в функциональном стиле?
Как взаимодействовать с базами данных в функциональном стиле?
… и другие практические моменты.
Не переключайтесь! Будет интересно!