Функциональная парадигма — это стиль разработки, в котором программа строится из комбинации функций. Её опорные идеи: чистые функции, иммутабельные данные, функции высшего порядка, композиция и декларативный подход: описываем «что нужно получить», а не «как и в каком порядке выполнять шаги».
Имутабельность: вместо изменения объекта создаётся новая версия. Это упрощает отладку и конкурентный доступ.
Функции высшего порядка: принимают/возвращают функции (например, map, filter, reduce).
Композиция: сборка сложного поведения из маленьких функций.
Декларативность: меньше инструкций управления потоком, больше описания преобразований данных.
Чистые функции: определение и практика
Чистая функция (референциально прозрачная) не зависит от внешнего изменяемого состояния и не производит побочных эффектов (I/O, логирование, изменение глобальных переменных, использование случайности и текущего времени).
Пример: простая чистая функция сложения
#include <iostream>
int add(int a, int b) { return a + b; }
int main() {
std::cout << add(2, 3) << "\n"; // 5
}
public class Main {
static int add(int a, int b) { return a + b; }
public static void main(String[] args) {
System.out.println(add(2, 3)); // 5
}
}
const add = (a, b) => a + b;
console.log(add(2, 3)); // 5
const add = (a: number, b: number): number => a + b;
console.log(add(2, 3)); // 5
package main
import "fmt"
func add(a, b int) int { return a + b }
func main() {
fmt.Println(add(2, 3)) // 5
}
Имутабельность: как обновлять без мутаций
Имутабельность уменьшает количество скрытых связей. Вместо «изменить объект» мы создаём «новый объект на основе старого».
Пример: обновление точки без мутаций
#include <iostream>
struct Point { int x; int y; };
int main() {
Point p1{1, 2};
Point p2{3, p1.y}; // новая версия с изменённым x
std::cout << p1.x << "," << p1.y << "\n"; // 1,2
std::cout << p2.x << "," << p2.y << "\n"; // 3,2
}
// Java 16+: record даёт иммутабельную модель данных
record Point(int x, int y) {}
public class Main {
public static void main(String[] args) {
var p1 = new Point(1, 2);
var p2 = new Point(3, p1.y());
System.out.println(p1.x() + "," + p1.y()); // 1,2
System.out.println(p2.x() + "," + p2.y()); // 3,2
}
}
type Point = { x: number; y: number };
const p1: Point = {x: 1, y: 2};
const p2: Point = {...p1, x: 3};
console.log(`${p1.x},${p1.y}`); // 1,2
console.log(`${p2.x},${p2.y}`); // 3,2
package main
import "fmt"
type Point struct { X, Y int }
func main() {
p1 := Point{1, 2}
p2 := Point{3, p1.Y} // копия со сменой X
fmt.Println(p1.X, p1.Y) // 1 2
fmt.Println(p2.X, p2.Y) // 3 2
}
Функции высшего порядка и композиция
Функции высшего порядка принимают/возвращают другие функции. Это позволяет собирать конвейеры преобразований: map → filter → reduce. Композиция — объединение простых функций в более сложную.
Каррирование и частичное применение
Каррирование превращает функцию f(a, b, c) в вид f(a)(b)(c). Частичное применение фиксирует некоторые аргументы и возвращает новую функцию. Это упрощает переиспользование и композицию.
Рекурсия: три обязательные части и практические аспекты
Рекурсия — функция вызывает сама себя для решения подзадачи меньшего размера. Любая корректная рекурсивная функция состоит из трёх обязательных частей:
Прерывание (база): условие, при котором возвращаем результат без дальнейших вызовов.
Логика шага: вычисления для текущего состояния (до/после рекурсивного вызова).
Повторный вызов: вызов той же функции с уменьшенной задачей, приближающейся к базе.
Пример: факториал (база, шаг, рекурсивный вызов)
#include <iostream>
long long fact(int n) {
if (n <= 1) return 1; // база
return n * fact(n - 1); // логика * рекурсивный вызов
}
int main() {
std::cout << fact(5) << "\n"; // 120
}
public class Main {
static long fact(int n) {
if (n <= 1) return 1; // база
return n * fact(n - 1); // логика * рекурсивный вызов
}
public static void main(String[] args) {
System.out.println(fact(5)); // 120
}
}
function fact(n) {
if (n <= 1) return 1; // база
return n * fact(n - 1); // логика * рекурсивный вызов
}
console.log(fact(5)); // 120
function fact(n: number): number {
if (n <= 1) return 1; // база
return n * fact(n - 1); // логика * рекурсивный вызов
}
console.log(fact(5)); // 120
package main
import "fmt"
func fact(n int) int {
if n <= 1 { return 1 } // база
return n * fact(n-1) // логика * рекурсивный вызов
}
func main() {
fmt.Println(fact(5)) // 120
}
Хвостовая рекурсия и безопасная альтернатива
Хвостовая рекурсия — рекурсивный вызов является последним действием. Теоретически компилятор/рантайм может оптимизировать её до цикла, но в перечисленных языках на это нельзя рассчитывать как на гарантированный механизм.
function factIter(n) {
let acc = 1;
for (let i = 2; i <= n; i++) acc *= i;
return acc;
}
console.log(factIter(5)); // 120
function factIter(n: number): number {
let acc = 1;
for (let i = 2; i <= n; i++) acc *= i;
return acc;
}
console.log(factIter(5)); // 120
public class Main {
static long factIter(int n) {
long acc = 1;
for (int i = 2; i <= n; i++) acc *= i;
return acc;
}
public static void main(String[] args) {
System.out.println(factIter(5)); // 120
}
}
#include <iostream>
long long factIter(int n) {
long long acc = 1;
for (int i = 2; i <= n; ++i) acc *= i;
return acc;
}
int main() {
std::cout << factIter(5) << "\n"; // 120
}
package main
import "fmt"
func factIter(n int) int {
acc := 1
for i := 2; i <= n; i++ { acc *= i }
return acc
}
func main() {
fmt.Println(factIter(5)) // 120
}
Ленивые вычисления (lazy) и потоки данных
Ленивость откладывает вычисления до момента, когда результат действительно нужен. Это полезно для бесконечных последовательностей или дорогих операций.