Что такое чистая функция?
Чистая функция (pure function) — это функция, которая при одинаковых входных данных всегда возвращает один и тот же результат и не имеет побочных эффектов. Это фундаментальное понятие в функциональном программировании, включая Scala, Elixir, Haskell, F#, OCaml и другие языки. Чистые функции позволяют писать код, который легко тестировать, масштабировать и отлаживать.
Два ключевых признака чистой функции
-
Детерминированность (deterministic)
Результат зависит только от входных параметров.
Если вызвать функцию f(2, 3) — она всегда вернёт одно и то же значение. -
Отсутствие побочных эффектов (no side effects)
Функция не изменяет внешнее состояние и не зависит от него.
Запрещено:-
Модифицировать глобальные переменные
-
Изменять аргументы по ссылке
-
Писать в файл, консоль, БД
-
Читать случайные данные или текущее время
-
Пример чистой функции:
def square(x: Int): Int = x \* x
-
Выход зависит только от x
-
Никаких побочных эффектов
-
Без доступа к внешнему миру
Пример нечистой функции:
var counter = 0
def increment(x: Int): Int = {
counter += 1
x + counter
}
-
Используется и изменяется глобальное состояние
-
При одном и том же x, результат может быть разным
-
Функция не является детерминированной
Побочные эффекты — что это?
Побочный эффект (side effect) — это любое воздействие функции на внешний мир или зависимость от него.
Примеры:
-
Печать в консоль (println)
-
Логгирование
-
Запись в файл или базу данных
-
Генерация случайного числа (Random.nextInt)
-
Чтение системного времени (System.currentTimeMillis)
-
Изменение аргументов по ссылке
Если функция делает хотя бы одно из вышеуказанного — она нечистая.
Почему важны чистые функции?
1. Тестируемость
Чистые функции легко тестировать — не нужно создавать мок-объекты, монтировать базу или подменять системное время.
assert(square(5) == 25)
2. Параллелизм и многопоточность
Чистые функции не используют или не изменяют глобальное состояние, поэтому их можно безопасно выполнять параллельно.
3. Переиспользуемость
Функции, не зависящие от контекста, легче повторно использовать в разных местах программы.
4. Кэширование (memoization)
Если функция всегда возвращает один и тот же результат, можно кэшировать результат и избежать повторных вычислений.
Чистые функции в Scala
Scala как мультипарадигменный язык позволяет писать как чистые, так и нечистые функции. Однако при функциональном стиле кодирования акцент делается именно на чистые.
Примеры:
// Чистая
def add(a: Int, b: Int): Int = a + b
// Чистая
def capitalize(s: String): String = s.toUpperCase
// Не чистая (пишет в консоль)
def greet(name: String): Unit = println(s"Hello, $name!")
Сравнение: чистая vs нечистая
Функция | Чистая | Побочный эффект |
---|---|---|
def add(x: Int, y: Int) = x + y | ✅ | ❌ |
--- | --- | --- |
def log(x: String) = println(x) | ❌ | ✅ (печать) |
--- | --- | --- |
def now() = System.currentTimeMillis | ❌ | ✅ (время) |
--- | --- | --- |
def sqrt(x: Double) = Math.sqrt(x) | ✅ | ❌ |
--- | --- | --- |
Можно ли писать полезную программу только на чистых функциях?
Формально — нет. Любая реальная программа должна взаимодействовать с внешним миром: печатать данные, читать файлы, принимать ввод от пользователя. Но эти части можно изолировать от логики и держать побочные эффекты на границах системы. Это делает большую часть кода чистой.
Например:
def calculateTax(income: Double): Double = income \* 0.13
def main(): Unit = {
val income = scala.io.StdIn.readDouble()
val tax = calculateTax(income)
println(s"Tax: $tax")
}
-
calculateTax — чистая
-
main — нечистая (ввод/вывод)
Higher-order функции и чистота
Многие функции из List, Seq, Option, Either работают с чистыми функциями:
List(1, 2, 3).map(x => x + 1) // List(2, 3, 4)
Здесь map ожидает чистую функцию (Int) => Int.
Связь с иммутабельностью
Чистые функции хорошо работают с неизменяемыми структурами данных (immutable structures), потому что не полагаются на изменение состояния. Это делает код безопасным в конкурентной среде.
Каррирование и чистые функции
Каррирование помогает разбивать чистые функции на части:
def multiply(a: Int)(b: Int): Int = a \* b
val timesTwo = multiply(2) _
val result = timesTwo(5) // 10
timesTwo — частично применённая чистая функция.