Функциональное программирование (ФП) — это стиль, в котором программу собирают из функций, вычисляющих результат только по своим аргументам, а данные не изменяют, а создают заново. Это не «программа из функций» вообще (процедурный код тоже состоит из функций), а дисциплина: без скрытого состояния и без побочных эффектов там, где их можно избежать.
Содержание
- Мини-словарь: пять терминов, которые путают
- Минимальный пример: императивно и функционально
- Принцип 1. Чистые функции и побочные эффекты
- Принцип 2. Неизменяемость
- Принцип 3. Функции высшего порядка и композиция
- Принцип 4. Рекурсия вместо циклов — и ее граница
- Тот же пример на Scala
- ФП, императивный стиль и ООП
- Языки с функциональным уклоном
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — четыре принципа ФП на одном сквозном примере со скидками на заказы: сначала на 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.



