Backend Typescript 1.0.0 Help

Функциональная парадигма

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

Ключевые идеи и почему это работает

  • Чистые функции: одинаковые входы → всегда одинаковый результат; побочные эффекты отсутствуют.

  • Имутабельность: вместо изменения объекта создаётся новая версия. Это упрощает отладку и конкурентный доступ.

  • Функции высшего порядка: принимают/возвращают функции (например, 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 } }
const p1 = {x: 1, y: 2}; const p2 = {...p1, x: 3}; console.log(p1.x + "," + p1.y); // 1,2 console.log(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 }

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

Функции высшего порядка принимают/возвращают другие функции. Это позволяет собирать конвейеры преобразований: mapfilterreduce. Композиция — объединение простых функций в более сложную.

Каррирование и частичное применение

Каррирование превращает функцию 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) и потоки данных

Ленивость откладывает вычисления до момента, когда результат действительно нужен. Это полезно для бесконечных последовательностей или дорогих операций.

Побочные эффекты: как ими управлять

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

  • Инъекция зависимостей: передавайте now(), rng(), клиенты БД параметрами.

  • Явные типы результатов: Option/Maybe, Either/Result, ошибки как значения (Go).

  • Чёткие слои: «порт» (эффект) и «ядро» (чистая логика).

Когда функциональный стиль особенно уместен

  • Трансформации коллекций, агрегации, фильтрации.

  • Валидация и нормализация входных данных.

  • Построение конвейеров (ETL, обработка событий).

  • Чистые вычислительные ядра, которые легко тестировать.

Практические советы

  • Стремитесь к референциальной прозрачности: одна и та же функция для одних и тех же аргументов возвращает один и тот же результат.

  • Сначала пишите чистую логику, затем «оборачивайте» её эффектами.

  • Предпочитайте неизменяемые структуры и readonly-контракты.

  • Стройте конвейеры из map/filter/reduce; дробите шаги на маленькие функции.

  • Для рекурсии всегда проверяйте: есть ли база, уменьшается ли задача, не переполнится ли стек.

Last modified: 01 October 2025