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

Функциональное программирование: принципы и примеры на Python и Scala Полезное

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

Ниже — четыре принципа ФП на одном сквозном примере со скидками на заказы: сначала на Python, затем на Scala. Примеры проверены 23.09.2026 на Python 3.14.7 и Scala 3.9.0 (scala-cli 1.17.1, JDK 25).

Мини-словарь: пять терминов, которые путают

Термин Что значит Пример
Чистая функция Результат зависит только от аргументов, и функция ничего не меняет снаружи lambda x: x * 0.9
Побочный эффект Любое наблюдаемое действие, кроме возврата значения: запись в файл, печать, изменение аргумента или глобальной переменной prices[i] = ..., print(...)
Неизменяемость Созданное значение нельзя поменять, можно только получить новое tuple, val в Scala
Функция высшего порядка Принимает функцию аргументом или возвращает функцию map, filter, reduce
Ссылочная прозрачность Вызов можно заменить его результатом, и поведение программы не изменится add(3, 5) -> 8

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

Минимальный пример: императивно и функционально

Задача: из списка сумм заказов взять заказы от 50 и сложить их со скидкой 10%.

from functools import reduce

orders = [120, 45, 300, 80, 15]

# Императивно: шаги и изменяемая переменная
total = 0
for amount in orders:
    if amount >= 50:
        total += amount * 0.9
print("imperative:", total)

# Функционально: описываем, ЧТО получить, из чистых функций
def is_big(amount):
    return amount >= 50

def with_discount(amount):
    return amount * 0.9

result = reduce(lambda acc, x: acc + x,
                map(with_discount, filter(is_big, orders)),
                0)
print("functional:", result)
print("orders:", orders)

Результат прогона:

imperative: 450.0
functional: 450.0
orders: [120, 45, 300, 80, 15]

Ответ одинаковый, разница в устройстве. В императивной версии важен порядок шагов и есть переменная total, которая меняется на каждой итерации. В функциональной — конвейер: filter отбирает (120, 300, 80), map применяет скидку (108, 270, 72), reduce сворачивает в одно число. Каждую из функций is_big и with_discount можно проверить отдельно, не запуская весь расчет.

В реальном Python-коде вместо reduce со сложением обычно пишут sum(...), а вместо map/filter — генераторное выражение. Это тот же функциональный стиль, просто идиоматичнее для языка.

Принцип 1. Чистые функции и побочные эффекты

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

rate = 0.9

def apply_discount(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * rate
    return prices

cart = [100, 200]
print(apply_discount(cart))
print(apply_discount(cart))
print("cart:", cart)
[90.0, 180.0]
[81.0, 162.0]
cart: [81.0, 162.0]

Два одинаковых вызова дали разный ответ, а корзина изменилась «сама». Исправление — все входы передавать параметрами и возвращать новый объект:

def apply_discount(prices, rate):
    return tuple(p * rate for p in prices)

cart = (100, 200)
print(apply_discount(cart, 0.9))
print(apply_discount(cart, 0.9))
print("cart:", cart)
(90.0, 180.0)
(90.0, 180.0)
cart: (100, 200)

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

Принцип 2. Неизменяемость

Вместо изменения объекта создают его новую версию. В Python для этого подходят tuple, frozenset и dataclass(frozen=True):

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Order:
    id: int
    amount: float
    status: str = "new"

o1 = Order(1, 120.0)
o2 = replace(o1, status="paid")
print(o1)
print(o2)
o1.status = "paid"
Order(id=1, amount=120.0, status='new')
Order(id=1, amount=120.0, status='paid')
Traceback (most recent call last):
  ...
dataclasses.FrozenInstanceError: cannot assign to field 'status'

replace вернул новый заказ, старый не изменился, а прямое присваивание упало с ошибкой. Оговорка: в Python неизменяемость неглубокая. Кортеж, внутри которого лежит список, запрещает заменить элемент, но сам список внутри менять можно.

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

Принцип 3. Функции высшего порядка и композиция

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

from functools import reduce

def compose(*funcs):
    return lambda x: reduce(lambda acc, f: f(acc), funcs, x)

normalize = compose(str.strip, str.lower, lambda s: s.replace(" ", "-"))
print(normalize("  Functional Programming "))

def make_multiplier(k):
    return lambda x: x * k

triple = make_multiplier(3)
print(list(map(triple, [1, 2, 3])))
functional-programming
[3, 6, 9]

compose собирает из трех простых функций одну, которая применяет их слева направо. make_multiplier возвращает новую функцию, запомнившую k (это замыкание).

Типичная ловушка Python: map и filter возвращают не список, а ленивый итератор, который проходится один раз.

big = filter(lambda x: x >= 50, [120, 45, 300])
print(list(big))
print(list(big))
[120, 300]
[]

Второй проход вернул пустой список без ошибки. Если результат нужен несколько раз, сразу сохраните его: big = list(filter(...)).

Принцип 4. Рекурсия вместо циклов — и ее граница

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

def total(xs):
    return 0 if not xs else xs[0] + total(xs[1:])

print(total(list(range(500))))
print(total(list(range(5000))))
124750
Traceback (most recent call last):
  ...
RecursionError: maximum recursion depth exceeded

У CPython лимит глубины рекурсии по умолчанию 1000 (sys.getrecursionlimit()), и оптимизации хвостовых вызовов нет. Поэтому в Python для обхода данных используют sum, reduce или цикл, а рекурсию оставляют для деревьев и задач с небольшой глубиной. В Scala хвостовую рекурсию компилятор превращает в цикл — ниже это видно.

Тот же пример на Scala

Scala — язык для JVM, где функциональный стиль встроен в стандартную библиотеку: List неизменяемый, val нельзя переприсвоить, а у case class есть copy. Файл Main.scala, запуск командой scala-cli run Main.scala:

import scala.annotation.tailrec

final case class Order(id: Int, amount: BigDecimal, status: String = "new")

def isBig(o: Order): Boolean = o.amount >= 50
def withDiscount(o: Order): Order = o.copy(amount = o.amount * BigDecimal("0.9"))

@tailrec
def sumAmounts(xs: List[Order], acc: BigDecimal = 0): BigDecimal = xs match
  case Nil          => acc
  case head :: tail => sumAmounts(tail, acc + head.amount)

@main def run(): Unit =
  val orders = List(Order(1, 120), Order(2, 45), Order(3, 300), Order(4, 80), Order(5, 15))

  val discounted = orders.filter(isBig).map(withDiscount)
  val total = discounted.map(_.amount).foldLeft(BigDecimal(0))(_ + _)

  println(discounted.map(_.amount))
  println(total)
  println(sumAmounts(discounted))
  println(orders.head)
  println(orders.head.copy(status = "paid"))
List(108.0, 270.0, 72.0)
450.0
450.0
Order(1,120,new)
Order(1,120,paid)

Разбор по строкам:

  • filter и map — те же операции, что в Python, но вызываются цепочкой методов;
  • foldLeft — аналог reduce со стартовым значением;
  • BigDecimal вместо Double — для денег, чтобы не ловить двоичную погрешность;
  • copy создает новый заказ, исходный orders.head не изменился;
  • @tailrec просит компилятор проверить, что рекурсия хвостовая, и превратить ее в цикл.

Что будет при нарушении правил — сообщит компилятор. Строка total = total + 1 после val total = 0 не соберется: [E052] Type Error ... Reassignment to val total. Если под @tailrec написать нехвостовую рекурсию (head + total(tail) — после вызова еще есть сложение), компиляция тоже упадет: Cannot rewrite recursive call: it is not in tail position. Исправление — перенести накопление в аргумент, как в sumAmounts.

ФП, императивный стиль и ООП

Критерий Императивный ООП Функциональный
Единица программы Последовательность команд Объект: данные + методы Функция
Состояние Изменяемые переменные Скрыто внутри объектов, обычно изменяемо По возможности неизменяемо
Повторение Циклы Циклы, итераторы map/filter/fold, рекурсия
Сильная сторона Прямой контроль над шагами Моделирование предметной области Предсказуемость, тестирование, параллельность

Это не взаимоисключающие подходы. Scala, Kotlin, F#, JavaScript, Python, C# и Java (лямбды и Stream API с Java 8) совмещают ООП и функциональные приемы в одном коде.

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

Языки с функциональным уклоном

Язык Особенность
Haskell Чисто функциональный, ленивые вычисления, эффекты выражены в типах
OCaml, F# Семейство ML: строгая типизация, F# работает на .NET
Erlang, Elixir Неизменяемые данные и легкие процессы для отказоустойчивых систем
Clojure, Common Lisp, Racket Семейство Lisp, Clojure работает на JVM
Scala ФП и ООП на JVM, широко применяется в обработке данных
Elm Чисто функциональный язык для веб-интерфейсов

Для изучения ФП не обязательно сразу брать Haskell: принципы из этой статьи применимы в Python и JavaScript уже сегодня.

Выводы

  • Функциональное программирование — это дисциплина: чистые функции, неизменяемые данные и эффекты, вынесенные на край программы.
  • Конвейер filter -> map -> reduce/foldLeft заменяет цикл с изменяемым счетчиком и делает каждый шаг отдельно тестируемым.
  • В Python ФП-приемы работают с оговорками: неизменяемость неглубокая, map/filter ленивы и одноразовы, хвостовая рекурсия не оптимизируется.
  • В Scala неизменяемость и проверка хвостовой рекурсии встроены в язык: ошибки ловит компилятор.
  • ФП не заменяет ООП, а дополняет его — большинство современных языков совмещают оба стиля.

Где применяется / связь с практикой

Освойте тему на практике

Функциональный стиль особенно заметен там, где важны предсказуемость и параллельность: обработка потоков данных, финансовые расчеты, распределенные бэкенды. Scala в этой нише используют для сервисов на JVM и конвейеров обработки данных. Если хочется разобрать язык системно — от коллекций и case-классов до эффектов и конкурентности, — посмотрите курс Otus по Scala.

Освойте тему на практике

Отдельные темы удобно попробовать на бесплатных открытых уроках Otus.

FAQ

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

Можно ли писать функционально на Java?
Да, частично: лямбды, Stream API, Optional и неизменяемые записи record позволяют строить конвейеры без изменяемого состояния, хотя язык остается объектно-ориентированным.

Что такое монада и нужна ли она новичку?
Это способ последовательно соединять вычисления с «контекстом» вроде ошибки или отсутствия значения (Option в Scala). Для первых шагов в ФП достаточно чистых функций, неизменяемости и map/filter/fold.

OTUS Журнал