Привет, Хабр! Меня зовут Артём Корсаков. Я руководитель группы серверной разработки в компании "Криптонит". Пишу на 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. Мы постоянно её пополняем и ждём вашей обратной связи!

А впереди — много практики! В следующих статьях мы разберём:

  • Как определять структуры данных?

  • Как взаимодействовать с конфигами в функциональном стиле?

  • Как взаимодействовать с базами данных в функциональном стиле?

  • … и другие практические моменты.

Не переключайтесь! Будет интересно!

Комментарии (11)


  1. Sirion
    13.10.2025 19:26

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

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


    1. Kazurus
      13.10.2025 19:26

      Rust достаточно много чего из фп взял и haskell в частности. Result, Option и прочие map, fold. В js/ts тоже полно элементов, или тот же react redux/hook. Не просто так в свежих языках (rust, go) отказались от ООП. Правда go и от фп отказался. А rust вполне гармонично некоторые концепции перенял


    1. CrazyOpossum
      13.10.2025 19:26

      Порог входа и, как следствие, бизнес причины. В Хаскелл (да и в целом в ФП) не вкатиться без образования, в отличие от какого-нибудь Го. Поэтому мало разработчиков, поэтому бизнес не вкладывается, и экосистемы развиваются энтузиастами в свободное время. Для низкоуровневого программирования не подходят из-за рантайма, но это не так важно.


      1. fonkost Автор
        13.10.2025 19:26

        Да, согласен! Самая большая проблема - высокий порог входа. Я пытаюсь на Scalabook как-то снизить этот порог входа. Время покажет, удастся ли мне это или нет.

        Большая проблема состоит в том, что вроде как с Java на Scala перейти не так сложно, но по факту получается, что разработчики продолжают писать на Java, только используя Scala. Как например, тут. Там от Scala только название языка, а так - чистая Java. Я пытался даже контрибьюить в такие репы, но это бесполезно без подробного объяснения на примерах, в чем плюсы и какие минусы в тех или иных подходах.


  1. Lewigh
    13.10.2025 19:26

    За последние лет 10 только ленивый не написал про волшебные чистые функции которые позволяют писать код без багов. Может быть все-таки сменить пластинку написать уже как это все будет выглядеть в реальном мире где на каждом шаге IO и во что эта красота чаще всего превращается?


    1. fonkost Автор
      13.10.2025 19:26

      Безусловно! Все так: надо подробно описывать, как все это выглядит в реальном мире и во что превращается. Я начал этот путь в отдельном разделе. С одной стороны рано давать ссылку, с другой - можно получить конструктивную обратную связь. Что касается IO, то это действительно проблема, т.к. часто разработчики пихают IO в те места, где лучше использовать другие концепции, например, Either, Validated, либо Stream. Это тоже отдельная большая тема, которую нужно обсуждать и как-то прояснять, где IO действительно используется, а где - лучше использовать что-то другое.


      1. Lewigh
        13.10.2025 19:26

        Благодарю за ссылку, с интересом ознакомлюсь.


    1. CrazyOpossum
      13.10.2025 19:26

      Так же как и в нормальном императивном коде. IO выносятся в свои слои, а логика чистая. Ввод - это прочитали из потока, распарсили в свои типы, всё, отдали в логику. То есть нагрузка здесь только в парсинге входных данных. Вывод - то же самое. Частый вопрос - что делать с логированием, метриками, исключениями, всяким таким. Для каждого своя монада, которая "запускается" в "грязном" IO коде, но для логики выглядит как чистая абстракция. Дальше нужно смотреть на саму задачу, потому что соотношение логики/io может быть от 99 к 1 до наоборот.


  1. I-AV
    13.10.2025 19:26

    Думаю, что ФП не выглядит мейнстримом потому, что чаще всего оказывается под капотом бэкенда, который не видит обычный пользователь. Например, в X (Twitter) ради устойчивости к нагрузкам часть кода переписали с Ruby на Scala. Серверная часть WhatsApp написана на функциональном языке Erlang. Многие компании сейчас используют Apache Spark — фреймворк на Scala. Если для написания пользовательских приложений ФП может быть избыточным вариантом, то для критической инфраструктуры и в мире высоконагруженных систем он рулит.


  1. sergehog
    13.10.2025 19:26

    Как только начинаешь писать по-настоящему сложные проекты, чистые функции уходят на задний план. Чистая архитектура - вот что важно. State в любом случае будет. А к стейту прилагаются структуры данных. Без них никак. Там и инкапсуляция/методы органично вписываются. Вдруг неожиданно выяснится, что нужны независимые модули, чтоб код был тестируемым и легко изменяемым. А там и до абстрактных классов/интерфейсов рукой подать. Вот тут то c++ даст прикурить любому мажорскому или хипстерскому языку.


    1. fonkost Автор
      13.10.2025 19:26

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

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

      Кстати, у функционального программирования богатая история.