Функциональное программирование в TypeScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в TypeScript: продвинутый уровень

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

1. Иммутабельность и структурный обмен данными

Иммутабельность (неизменяемость) — краеугольный камень ФП. В TypeScript мы можем гарантировать неизменность на уровне типов с помощью readonly и ReadonlyArray. Однако для эффективной работы с большими структурами данных (например, Redux store) используется структурный обмен (structural sharing) — библиотеки вроде Immer или Immutable.js.

Рассмотрим пример с использованием readonly и функции для обновления вложенного объекта без мутации:

type User = {
  readonly id: number;
  readonly name: string;
  readonly address: {
    readonly city: string;
    readonly zip: string;
  };
};

const updateUserCity = (user: User, newCity: string): User => ({
  ...user,
  address: {
    ...user.address,
    city: newCity,
  },
});

const user: User = { id: 1, name: 'Alice', address: { city: 'NYC', zip: '10001' } };
const updatedUser = updateUserCity(user, 'LA');
console.log(user.address.city); // NYC (оригинал не изменился)
console.log(updatedUser.address.city); // LA

Обратите внимание: мы создаем новый объект, но повторно используем неизмененные части (например, id и name). Это и есть структурный обмен вручную. Для глубокой вложенности лучше применять библиотеки.

2. Функторы и их реализация

Функтор — это контейнер, который реализует метод map, позволяющий применять функцию к содержимому контейнера, не нарушая его структуры. В TypeScript типичный функтор — это Array (массив). Но мы можем создать свой функтор, например, Maybe, чтобы избежать ошибок с null и undefined.

Реализуем простой функтор Maybe:

class Maybe<T> {
  private constructor(private value: T | null) {}

  static of<U>(val: U | null | undefined): Maybe<U> {
    return new Maybe(val ?? null);
  }

  map<U>(fn: (val: T) => U): Maybe<U> {
    if (this.value === null) {
      return Maybe.of<U>(null);
    }
    return Maybe.of(fn(this.value));
  }

  getOrElse(defaultValue: T): T {
    return this.value !== null ? this.value : defaultValue;
  }
}

// Использование
const result = Maybe.of('hello')
  .map(s => s.toUpperCase())
  .map(s => s + ' world')
  .getOrElse('fallback');
console.log(result); // "HELLO WORLD"

const empty = Maybe.of<string>(null)
  .map(s => s.toUpperCase())
  .getOrElse('default');
console.log(empty); // "default"

Функтор Maybe позволяет безопасно цепочечно вызывать функции, не беспокоясь о null. Каждый map возвращает новый Maybe, сохраняя контекст.

3. Монады: цепочки операций с побочными эффектами

Монада — это функтор, который также реализует метод flatMap (часто называемый bind или chain). Он позволяет "разворачивать" вложенные контейнеры. Классический пример монады — Promise или Array. Рассмотрим монаду Either для обработки ошибок без исключений.

type Either<L, R> = Left<L> | Right<R>;

class Left<L> {
  readonly value: L;
  constructor(value: L) { this.value = value; }
  isLeft(): this is Left<L> { return true; }
  isRight(): this is Right<never> { return false; }
  map<R2>(_fn: (val: never) => R2): Either<L, R2> { return this as any; }
  flatMap<R2>(_fn: (val: never) => Either<L, R2>): Either<L, R2> { return this as any; }
}

class Right<R> {
  readonly value: R;
  constructor(value: R) { this.value = value; }
  isLeft(): this is Left<never> { return false; }
  isRight(): this is Right<R> { return true; }
  map<R2>(fn: (val: R) => R2): Either<never, R2> { return new Right(fn(this.value)); }
  flatMap<L2, R2>(fn: (val: R) => Either<L2, R2>): Either<L2 | never, R2> { return fn(this.value); }
}

// Вспомогательные функции
const left = <L, R = never>(val: L): Either<L, R> => new Left(val);
const right = <R, L = never>(val: R): Either<L, R> => new Right(val);

// Использование для деления с проверкой на ноль
const safeDivide = (a: number, b: number): Either<string, number> => {
  if (b === 0) return left('Division by zero');
  return right(a / b);
};

const compute = (x: number): Either<string, number> => {
  return safeDivide(10, x)
    .flatMap(result => safeDivide(result, 2))
    .map(r => r * 100);
};

console.log(compute(2)); // Right { value: 250 }
console.log(compute(0)); // Left { value: "Division by zero" }

Монада Either позволяет обрабатывать ошибки как данные, а не через try/catch. Это делает код более предсказуемым и композируемым.

4. Каррирование, композиция и point-free стиль

Каррирование — преобразование функции с несколькими аргументами в последовательность функций с одним аргументом. Композиция — объединение функций, где выход одной становится входом другой. Point-free стиль (бесточечная нотация) позволяет писать код без явного указания аргументов.

Реализуем утилиты для кар

Блоги

Рекомендаций по книгам