Мотивацией для написания этого поста стали два года собеседований JS/TS-инженеров. Я интересуюсь языками и функциональным программированием, поэтому всегда «разбавлял» технические вопросы разговором о парадигмах. И заметил любопытную асимметрию.

Об ООП кандидаты рассуждали уверенно — но в основном на концептуальном уровне, не вдаваясь в то, как именно ООП реализовано в JavaScript. С FP картина была другой: уверенности меньше, зато критика — конкретная и повторяющаяся: «иммутабельность дорогая по памяти», «рекурсия небезопасна из-за стека». Что характерно — эти аргументы почти всегда были сформулированы через опыт работы с JS, а не с Haskell, Clojure или Scala.

Это важная деталь. Любая парадигма, на мой взгляд, существует как минимум на двух уровнях: концептуальном (идеальная модель) и имплементационном (как конкретный язык эту модель выражает). Судить о FP по JS — примерно то же самое, что судить об ООП по bash-скриптам с глобальными переменными.

Параллельно я регулярно слышал, что JS — функциональный язык. Аргументы варьировались от «там есть .map()» до рассуждений о чистых функциях и каррировании. Именно это и стало поводом для поста: я хочу объяснить, что я считаю функциональным языком — и почему JS таковым не является. Не перечислить отсутствующие фичи, а показать, почему их нет и что это значит в реальном рантайме.

Уточнение: далее JS и TS используются как взаимозаменяемые понятия, кроме случаев, когда речь идёт о системе типов — тогда я указываю TS явно.


1. Мутабельность по умолчанию

В Haskell вы физически не можете изменить переменную. В Clojure все базовые структуры иммутабельны из коробки. В JS всё строго наоборот:

const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [1, 2, 3, 4]

const user = { name: "Alice" };
user.name = "Bob"; // Работает

Пример выше - с массивом, но ситуация с объектами, мапами, сэтами - аналогичная.

const — это запрет на переприсваивание ссылки, а не на мутацию данных. Проще говоря: коробку менять нельзя, а всё что внутри — пожалуйста.

Так же, в Scala cуществуют кейс-классы (case class), которые буквально являются способом для моделирования иммутабельных данных:

case class User(name: String)
val alice = User("Alice")
val bob = alice.copy(name = "Bob") // alice не изменился

Почему это важно? Мутабельность по умолчанию ломает ссылочную прозрачность (это возможность заменить выражение его значением без изменения поведения программы) и делает невозможными гарантии, на которых строится функциональный дизайн. Да, есть Object.freeze() и библиотеки вроде Immutable.js. Но они — костыли поверх языка, спроектированного с мутабельностью в голове. Решения об использовании подобных библиотек полностью лежит на конкретной команде и никак не "форсируется".

Почему так? JS создавался в 1995 году за 10 дней как скриптовый язык для браузера. Модель «всё мутабельно и лежит в куче» была самой простой для реализации и понятной для программистов, привыкших к C/Java. Перепроектировать модель памяти спустя 30 лет, не сломав веб — невозможно.


2. Нет оптимизации хвостовой рекурсии (TCO)

На мой взгляд, это один из самых убедительных аргументов: TCO вошёл в стандарт ES2015. Сегодня его поддерживает только Safari. V8 (Chrome, Node.js) — нет. SpiderMonkey (Firefox) — нет.

function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc); // хвостовой вызов — но TCO не работает
}
factorial(100000); // RangeError: Maximum call stack size exceeded

В Scala та же функция с аннотацией @tailrec не просто работает — компилятор гарантирует оптимизацию ещё до рантайма:

import scala.annotation.tailrec

@tailrec
def factorial(n: Int, acc: Long = 1): Long = {
  if (n <= 1) acc
  else factorial(n - 1, n * acc)
}

factorial(100000) // Работает корректно
// Если хвостовой вызов невозможен — ошибка компиляции,
// не RangeError на продакшене

Ключевой момент: в Scala вы узнаёте о проблеме в момент написания кода. В JS — в рантайме.

Почему V8 не делает TCO? Это же просто замена вызова на jmp?

Технически — да. Но в динамическом языке с eval, Function.caller и DevTools есть нюансы:

  1. Потеря стектрейсов. При TCO хвостовой вызов заменяет текущий фрейм. В стеке остаётся только один фрейм вместо цепочки. Для отладки это катастрофа. В Scala такой проблемы нет, т.к. компилятор просто превращает хвостовую рекурсию в обычный while на этапе сборки.

  2. Совместимость со старыми API и отладка. В JS существуют устаревшие API вроде Function.caller, которые позволяют узнать, кто вызвал функцию. TCO разрушает эту информацию, так как стирает историю вызовов.

  3. Сложность реализации в JIT. V8 использует многоуровневую компиляцию (Ignition → Sparkplug → Maglev → TurboFan). TCO требует пересмотра того, как генерируются и инвалидируются деоптимизированные фреймы.

Инженеры V8 открыто заявляли: цена реализации TCO в текущей архитектуре превышает пользу для экосистемы.


3. Ленивые и персистентные коллекции

Посмотрим на пиковое потребление памяти и на то, как вообще выполняется типичная JS-цепочка:

const result = hugeArray
  .filter(x => x > 0)   // [1] создаётся массив A
                         //     в памяти: hugeArray + A
  .map(x => x * 2)      // [2] создаётся массив B
                         //     в памяти: hugeArray + A + B
                         //     A больше не нужен — но GC ещё не пришёл
  .filter(x => x < 100) // [3] создаётся массив C
                         //     в памяти: hugeArray + B + C (+ возможно A)
  .slice(0, 10);         // [4] создаётся result из 10 элементов
                         //     C больше не нужен

В худшем случае — момент между шагами 2 и 3 — в памяти одновременно живут hugeArray, A и B. GC не синхронный: массив помечается как кандидат на удаление в момент, когда на него перестают ссылаться, но реально освобождается позже — по собственному расписанию движка. На больших данных это означает реальный memory spike в середине цепочки, даже если финальный результат крошечный.

В Scala .view превращает цепочку в единый поэлементный конвейер без промежуточных коллекций:

val result = hugeArray.view
  .filter(_ > 0)
  .map(_ * 2)
  .filter(_ < 100)
  .take(10)
  .toList
// Каждый элемент проходит через все три операции ровно один раз.
// Как только набрано 10 элементов — обработка прекращается.

Что насчёт генераторов?

В JS есть генераторы, и технически они позволяют сделать нечто похожее:

function* lazyPipeline(arr, filterFn, mapFn) {
  for (const x of arr) {
    if (filterFn(x)) {
      yield mapFn(x);
    }
  }
}

const result = [];
let count = 0;
for (const item of lazyPipeline(hugeArray, x => x > 0, x => x * 2)) {
  result.push(item);
  if (++count >= 10) break;
}

Это работает, но обратите внимание на то, во что превратился код: вместо декларативной цепочки — ручной цикл со счётчиком и break. Генераторы — это низкоуровневый примитив, а не стандартный API коллекций. В Scala .view — одно слово, встроенное в язык. В JS — отдельная функция-генератор, которую нужно написать самому, и императивный цикл снаружи. Разница не в том, можно ли — а в том, насколько это естественно и / или декларативно.

Примечание: после написания статьи уточнил, что в ES2025 в JS добавили Iterator Helpers, которые дают вам ленивое вычисление и потенциально бесконечные стримы, поэтому для точности решил указать это здесь. Они решают проблему с довольно специфическим и сложночитаемым синтаксисом, который приведён выше, но не решают проблему, описанную ниже.

Structural Sharing

// list1 — уже существующий список
val list1 = List(2, 3, 4)

// Добавляем 1 в голову — получаем list2
val list2 = 1 :: list1  // List(1, 2, 3, 4)

// В памяти создался ровно один новый узел — голова со значением 1.
// Хвост (2 -> 3 -> 4) не копировался — list2 просто ссылается на list1.

// list2: [1] -> [2] -> [3] -> [4]
//                ↑
//         здесь начинается list1
//         оба списка живут одновременно

O(1) по памяти и времени. Для более сложных структур (Scala Vector, Clojure Persistent Collections) используется структурный обмен на основе префиксных деревьев. При «изменении» элемента копируется только путь от корня до листа — O(log n). Остальной граф переиспользуется по ссылке:

val v1 = Vector(1, 2, 3, 4, 5)
val v2 = v1.updated(2, 99) // "меняем" третий элемент
// v1 = Vector(1, 2, 3, 4, 5) — не изменился
// v2 = Vector(1, 2, 99, 4, 5)
// Скопировано: O(log n) узлов. Остальное — общие ссылки.

В JS [...arr] — всегда полная копия. Персистентных структур с structural sharing нет из коробки.

Почему JS не делает ленивые коллекции по умолчанию?

  • Eager evaluation дружит с CPU-кэшем. Массив в JS — непрерывный кусок памяти (FixedArray / Fast Elements). Проход по нему предсказуем для prefetcher’а. Ленивый конвейер на генераторах порождает много мелких вызовов итераторов, что разрушает локальность данных.

  • JIT-оптимизации массивов. V8 агрессивно инлайнит методы Array.prototype. Для ленивых цепочек таких оптимизаций нет.

  • Structural sharing vs cache locality. Персистентные структуры используют деревья с широким ветвлением. Доступ к элементу — несколько разыменований указателей. На массиве — один offset. Для UI-рендеринга, где данные читаются линейно, массивы с копированием могут быть быстрее, несмотря на аллокации.

Язык оптимизирован под мейнстримный сценарий, а не под обработку больших данных.


4. Ошибки - не значения

Во многих языках программирования ошибки — это просто данные. В JavaScript ошибки — исключения.

Рассмотрим простую операцию:

// JSON.parse: (string) => any
const data = JSON.parse(userInput);

JSON.parse может выбросить ошибку, но это скрыто от системы типов. Сигнатура (string) => any говорит: «Я всегда возвращаю значение». На деле эта функция может бросить исключение, если в процессе парсинга строки что-то пойдет не так. Исключение — это незаявленный управляющий эффект: он невидим для компилятора, не отражён в типе и не вынуждает вызывающий код его обработать. На моей практике это довольно частый источник багов (люди - не роботы, забудете обернуть в try/catch, - получите exception).

Кроме прочего, конструкция throw не ссылочно-прозрачна по определению и в целом может вести себя своеобразно:

throw 1 // работает
throw "asd" // работает
throw new Error("что-то не так") // работает
throw [] // порядок
throw {} // тоже порядок

В TS не существует никакого контракта на то, что именно будет выброшено — ни в типе, ни в сигнатуре:

function getUser(id: string): User {
  // Выглядит безопасно.
  // Может бросить исключение. Вы не знаете.
}

Taким образом, throw переводит функцию из полной (total) в частичную (partial) и мы никаким образом не можем узнать об этом, кроме как прочитать всё тело функции. Это в буквальном смысле "слепая зона" системы типов.

В функциональных языках ошибка закодирована в возвращаемом типе:

val result: Try[Json] = Try(parse(userInput))

val upperName = result.map(_.user.name.toUpperCase)
// По-прежнему Try — Success или Failure

Ошибки — это значения, они композируются. Тип Try[Json] честно сообщает: «это вычисление может не получиться» — и компилятор не даст вам обратиться к результату, не обработав оба случая.

Инструкции против выражений

В ФП всё является значением. В JS обработка ошибок (как и ifwhilefor) — нет.

// В JS невозможно
const result = try {
  riskyOperation()
} catch (e) {
  handleError(e)
}

try/catch — это инструкция, а не выражение, её нельзя композировать и приходится разрывать "поток".

Обходной путь

Библиотеки вроде fp-ts или Effect возвращают ошибки как данные:

import { pipe } from 'fp-ts/function'
import * as E from 'fp-ts/Either'

// Функция-обёртка для обработки потенциальной ошибки
const parseJSON = (input: string): E.Either<Error, any> =>
  E.tryCatch(
    () => JSON.parse(input),
    (reason) => reason instanceof Error ? reason : new Error(String(reason))
  )

const result = pipe(
  parseJSON(input),
  E.map(data => data.user.name.toUpperCase())
)
// result: E.Either<Error, string>

Но обратите внимание на ключевую деталь: вам пришлось вручную обернуть JSON.parse. Сам язык остаётся неосведомлённым об эффектах.

Почему так сложилось?
Исключения в JavaScript — это механизм потока управления, а не модель данных. Они пришли из C++/Java 90-х, где цели были иными:

  • избежать загрязнения возвращаемых типов

  • обрабатывать «исключительные» ситуации без ручных проверок повсюду

  • быстро и дёшево раскручивать стек

Такая модель имела смысл для скриптового языка в браузере:

  • большинство отказов были внешними (сетевые ошибки, действия пользователя)

  • накладные расходы по памяти имели значение

  • явные типы ошибок усложнили бы простой код

JavaScript унаследовал эту модель — и она прижилась.

5. Нет синтаксической поддержки монад

Технически Promise — это монада (почти). Array с .flatMap() — тоже монада. JS позволяет выражать монадические паттерны. Но между «позволяет» и «поддерживает» — пропасть.

В Scala есть for-comprehension:

val result = for {
  user    <- findUser(id)      // Option[User]
  address <- user.address      // Option[Address]
  city    <- address.city      // Option[String]
} yield city

В JS то же самое — вложенные .then() или .flatMap(). Язык не знает, что вы работаете с монадой, и никак вам в этом не помогает.

Почему в JS нет сахара для монад? Потому что монады — абстракция над типами высшего порядка (HKT). А их нет (см. пункт 6). Нечто похожее на for-comprehension есть в промисах, специальный синтаксис async/await. Но это никак нельзя назвать "общим" механизмом, это буквально частный случай.


6. Отсутствие типов высшего порядка (Higher-Kinded Types)

Напишем немного абстракций: в Scala вы можете объявить обобщённый функтор:

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

Это значит: «Для любого типа F, принимающего один параметр, я могу определить, как работает map». Будь то List, Option, Future — одна абстракция, работающая для всех.

TypeScript этого не умеет. Библиотека fp-ts вынуждена эмулировать HKT через ручной реестр типов:

// Шаг 1: реестр — словарь вида "строка → реальный тип"
// Это единственный способ научить TS понимать, что 'Array' — это Array<A>
interface URItoKind<A> {
  readonly Array: Array<A>
  readonly Option: Option<A>
  // каждый новый тип регистрируется здесь вручную
}

// Шаг 2: URIS — это просто объединение всех зарегистрированных строк
// type URIS = 'Array' | 'Option' | ...
type URIS = keyof URItoKind<unknown>

// Шаг 3: Kind — это indexed access type (lookup по реестру)
// Kind<'Array', number>  → URItoKind<number>['Array']  → Array<number>
// Kind<'Option', string> → URItoKind<string>['Option'] → Option<string>
// Именно здесь строка превращается обратно в реальный generic-тип
type Kind<F extends URIS, A> = URItoKind<A>[F]

// Шаг 4: теперь можно написать "обобщённый" Functor
// но кавычки здесь не случайны — это не настоящий type constructor polymorphism,
// а его эмуляция через таблицу строк
interface Functor<F extends URIS> {
  map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>
}

Для меня это выглядит как борьба с ограничениями языка, причём борьба, которую авторы fp-ts ведут с гениальностью и упорством. Несмотря на действительно очень креативное решение, выглядит и читается подобное сложно, в особенности для тех людей, кто прежде вообще не сталкивался с типами высшего порядка и не до конца понимает, зачем они могут быть нужны.

Почему TypeScript не добавляет HKT? TS — надмножество JavaScript со структурной типизацией. HKT требуют kind-полиморфизма в компиляторе. Внедрение этого в структурную систему потребовало бы фундаментального пересмотра алгоритма вывода типов. Команда TS обсуждала это и пришла к выводу, что цена слишком высока для типичного TS-проекта.

Отдельного упоминания заслуживает Effect TS — современная библиотека, которая идёт в обход проблемы HKT совершенно иначе. Вместо эмуляции через URI-реестр она строит собственную систему эффектов поверх одного центрального типа Effect<A, E, R>, который кодирует сразу успех, ошибку и зависимости. По сути это полноценный effect system в духе ZIO из Scala — со structured concurrency, dependency injection через контекст и composable error handling. Effect не притворяется, что решает проблему HKT в общем виде, но для задачи «писать надёжный, композируемый код с управляемыми эффектами» предлагает более честный и практичный ответ, чем fp-ts. Показательно, что он набирает популярность именно среди тех, кто приходит в TS из Scala или Haskell и не готов мириться с процедурным хаосом.

В рамках дискуссии в комментариях под статьей услышал ряд критических аргументов относительно этого пункта, с которыми в целом согласен: отсутствие HKT - это скорее ограничение системы типов, которое не позволяет вам писать мощные обобщенные абстракции(которые иногда свойственны ФП), но вряд ли это тянет на полноценный маркер "нефункциональности" языка.


7. Отсутствие pattern matching

Этот аргумент, на мой взгляд, относительно слабее остальных, но всё же: во многих функциональных языках паттерн-матчинг — первоклассная конструкция языка. В Scala это выглядит так:

sealed trait Shape
case class Circle(radius: Double)          extends Shape
case class Rectangle(w: Double, h: Double) extends Shape

def area(s: Shape): Double = s match {
  case Circle(r)       => Math.PI * r * r
  case Rectangle(w, h) => w * h
  // Добавите Triangle и забудете здесь — компилятор предупредит
}

Обратите внимание на несколько вещей одновременно: сопоставление по структуре, автоматическая деструктуризация, и sealed — явно выраженное намерение что иерархия закрыта: расширить её можно только в рамках того же файла. Сам по себе sealed не имеет прямого отношения к pattern matching, но они часто используются вместе для безопасности и удобства.

В JS есть switch/case, но его возможности ощутимо скромнее.

Есть TC39 proposal на паттерн-матчинг — Stage 1 уже несколько лет. Реализация упирается в фундаментальный вопрос: что считать «типом» для сопоставления в динамическом языке без sealed traits? Каждый вариант дизайна ломает чьи-то ожидания.


8. "Гравитация языка"

Язык формирует стиль кода и культуру — не через запреты, а через то, что в нём естественно.

В Haskell, Clojure функциональный стиль — это единственный путь. Scala, будучи мультипарадигменным языком, дает выбор: можно писать в объектно-ориентированном стиле, как в Java, используя изменяемые переменные и наследование. Многие годы такие фреймворки, как Play и Akka(которая была ОЧЕНЬ популярна до скандала с лиценизиями), активно использовали эту возможность. Однако "гравитация языка" и современной экосистемы (Cats Effect, ZIO) направлена в другую сторону. Неизменяемые структуры данных, pattern matching и чистые функции в Scala реализованы настолько удобно и естественно, что путь наименьшего сопротивления ведёт именно к ним. Писать в ООП-стиле можно, но это требует сознательного усилия и всё чаще воспринимается как борьба с течением языка.

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

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

Это влияет на кодовую базу, командную культуру и архитектуру. В JS/TS я потратил неприличное количество времени на объяснение коллегам, зачем нужны чистые функции, монады, и почему мутация может быть серьёзной проблемой. По сути, я просто плыл против течения. В Scala или Haskell этот разговор просто не нужен. Когда язык не даёт нативных инструментов для FP, функциональная культура не формируется органически. Вместо неё — процедурный код с парой .map() для приличия и // TODO: refactor в конце файла, которому уже три года.


Итог

JavaScript — мощный, гибкий, мультипарадигменный язык. Но функциональным языком он не является. Не потому что в нём нельзя писать функционально, а потому что он не был спроектирован для этого.

Каждое из ограничений — не баг и не просчёт. Это результат осознанных компромиссов между производительностью для типичных веб-задач, обратной совместимостью с гигантской экосистемой и простотой отладки в DevTools.

Понимать эти компромиссы важно — особенно когда кто-то делает выводы о функциональном программировании в целом, глядя только на JS. FP — это не про .map() и стрелочные функции. Это про другую модель вычислений, которую JS по объективным причинам поддерживает лишь частично.

Комментарии (30)


  1. mitya_k
    18.04.2026 21:02

    Пункт 7

    Можно добавить default для того чтобы компилятор сказал, что есть необработанный case:

    // здесь будет ошибка компиляции, если не все case описаны
    default:
      const _exhaustiveCheck: never = shape;
      throw new Error(`Unhandled shape kind`);



    1. divideme Автор
      18.04.2026 21:02

      Да, это рабочий паттерн и важное уточнение, спасибо — мне стоило про это упомянуть. Но то, что я сам про это забыл, лишний раз доказывает тезис: язык мне здесь не помогает, это просто соглашение, которое нужно держать в голове и применять в каждом switch вручную.

      Ну и проблему открытой иерархии это, к сожалению, не решает.


  1. orenty7
    18.04.2026 21:02

    В функциональном программировании ошибки — это просто данные. В JavaScript ошибки — это “взрывы” потока управления.

    В хаскеле ошибки не выносятся в сигнатуры. Да, можно сделать функцию Either, но можно и кинуть исключение, которое кто-то должен словить. Более того, исключения даже можно кидать в другой тред

    6. Нет типов высшего порядка (Higher-Kinded Types)

    В окамле HKT тоже нету, при этом это вполне себе функциональный ЯП.


    1. divideme Автор
      18.04.2026 21:02

      По Haskell абсолютно справедливое замечание, стоило быть точнее. С одним лишь важным нюансом: насколько я знаю, Haskell "культурно поощряет" именно стиль с использованием Either / Maybe монад.

      По ОCaml и HKT - супер важное уточнение. В таком случае отсутствие HKT - это больше ограничение выразительности при написании обобщённых абстракций, а не маркер "функциональности" языка как таковой. Поправлю формулировку, спасибо!


      1. orenty7
        18.04.2026 21:02

        С одним лишь важным нюансом: насколько я знаю, Haskell “культурно поощряет” именно стиль с использованием Either / Maybe монад.

        И да, и нет, сильно зависит от задачи, имхо. Если в задаче подразумевается возможный возврат ошибки, например, при написании каких-нибудь парсеров, валидаторов, тайпчекеров, etc, возвращать Maybe/Either норм. Если не подразумевается, например, делаем http запрос и внезапно упала сеть, или функцию вызвали с не теми параметрами, то лучше кинуть ошибку. Короче, на мой взгляд он не поощряет, а, скорее, просто позволяет удобно пользоваться и тем, и тем


      1. Ilusha
        18.04.2026 21:02

        Мне в таком же стиле LLM отвечает. Важное уточнение, супер-важное уточнение.


  1. massivefpfan
    18.04.2026 21:02

    Вы буквально сформулировали в этой статье всё то, что уже год живет в моей голове, почти слово в слово! Всегда удивлялся, когда смотрел записи собеседований на ютубе, что даже очень опытные программисты на JS претендующие на огромные зарплаты очень своеобразно отвечают на вопросы про фп, в особенности когда их спрашивают о недостатках парадигмы. Скажите, есть ли у вас опыт использования fp-ts или effect-ts в продакшене?


    1. divideme Автор
      18.04.2026 21:02

      Effect-ts в продакшене попробовать не довелось, а вот с fp-ts поработал на двух проектах.

      На мой взгляд, библиотека очень мощная, но с онбордингом возникали определенные сложности. Если люди прежде не писали на функциональных языках, код на fp-ts для них выглядит тяжеловесно. И местами, надо признать, это ощущение вполне заслуженно — порог входа высокий и код визуально кажется совсем "чужим", не похожим на привычный TS.

      Кроме того, есть сугубо практический нюанс интеграции. Допустим, мы пишем React-приложение. Подключаем React Query, которая, естественно, ничего не знает про монады Task / TaskEither. Приходится писать адаптеры, превращающие ТЕ обратно в обычные Promise. Да, это пара строк кода, но React Query — не единственная такая библиотека. Любой хук форм или роутер заставляет держать в голове эти пограничные прослойки.

      В итоге появляется постоянный оверхед (пусть и небольшой), оправданность которого в команде — вопрос дискуссионный. Лично я считал, что строгость типов и обработка ошибок того стоят, кто-то другой скажет, что нет. Собственно, это прямое продолжение того, что я в статье назвал «гравитацией языка» — экосистема постоянно тянет тебя обратно в императивный мир.


  1. DmitryOlkhovoi
    18.04.2026 21:02

    Впервые слышу, чтобы JS называли функциональным)) Ну вы можете конечно обмазаться rx.js, immutable.js. На чистом JS, никто не пытался прикинуться функциональщиком) Еще и без длинных носков

    Но они — костыли поверх языка, спроектированного с мутабельностью в голове. Решения об использовании подобных библиотек полностью лежит на конкретной команде и никак не "форсируется".

    То поверху, то понизу - если вы хотите ФП на JS, по каким либо причинам вместо clojurescript или другого вообще стека, то это нормальные либы. Костыли или нет, это уже, что-то на снобском

    Каждое из ограничений — не баг и не просчёт.

    Да и хватит писать посты с ллмками, отвратительно.


  1. divideme Автор
    18.04.2026 21:02

    Впервые слышу, чтобы JS называли функциональным

    Этот тезис «кочует» по страницам книг, звучит в публичных выступлениях, есть даже целые курсы, в т.ч. от крупных и известных платформ вроде «frontend masters» где это мнение присутствует, так же я сослался на свой личный опыт проведения интервью. Я к тому, что у нас с вами опыт может сильно отличаться и это вполне нормально. В то же время то, что вы не слышали тезис «JS - функциональный язык» не означает, что этого тезиса не существует.

    То поверху, то понизу - если вы хотите ФП на JS, по каким либо причинам вместо clojurescript или другого вообще стека, то это нормальные либы. Костыли или нет, это уже, что-то на снобском

    Всё это - ваше личное оценочное суждение, не более. Никакого снобизма тут не было: я лишь констатировал факт того, что «из коробки» в языке решений не существует, требуется использование дополнительных инструментов. Некоторые из них абсолютно гениальные(effect ts, fp-ts), некоторые, на мой взгляд, менее удачные.


  1. ermouth
    18.04.2026 21:02

    фундаментальный вопрос: что считать «типом» для сопоставления в динамическом языке без sealed traits

    По мне так то, как в erlang реализован pattern matching – вполне себе распрекрасный вариант, для js в общем есть даже всякие подобия типа https://github.com/natefaubion/matches.js/.


  1. kmatveev
    18.04.2026 21:02

    Неплохо.

    Не согласен с заявлением, что functor - это high-kinded type, это typeclass, нет никаких высших порядков.

    И вот это позабавило:

    Язык формирует стиль кода и культуру — не через запреты, а через то, что в нём естественно.

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


    1. divideme Автор
      18.04.2026 21:02

      Я не утверждал, что функтор - это HKT. Я пытался на простом примере показать механизм системы типов, который позволяет выразить тайпкласс (т. е. что функтор параметризован HKT)


  1. Lewigh
    18.04.2026 21:02

    Тут вообще нужно смотреть с другой стороны. JS изначально даже не классический ООП язык. То что к нему впоследствии приделали классы сделало еще хуже. Получилась еще более кривая и проблемная пародия на Java. И вот из одной крайности, сообщество повело в другую. В ФП. Почему? Потому что это закрывает многие базовые проблемы JS. Например, функции как единицы построения программы подходят куда лучше кривых и проблемных объектов и классов в JS. Еще более важным моментом является использование концепции иммутабельности данных для закрытия проблем с работой в асинхронной среде.

    По поводу отсутствия хвостовой рекурсии, типов высших порядков, отсутствия паттерн матчинга - это все фигня и вообще не показатель функционального языка. Это скорей критерии чистого Haskell-подобного языка. Взять Scala, там исключений нет или есть мутабельность по умолчанию? Да там половина языка Java подобна, о чем говорить. В том же Clojure исключения есть, нет никакиких pattern matching, никаких игр с монадами и Higher-Kinded Types.

    По мне главные критерии ФП языка - это все-таки неизменяемость из коробки и логика языка в которой программа строиться вокруг функции как основного юнита.

    Главная проблема в сообществе JS - это попытка играть во "взрослый/чистый" ФП подобный Haskell не имея никаких средств поддержки языка. Отсюда выходит весь ад. Попытки играть в каррирование, в монады превращает код в адское нечитаемое месиво. Но это не особенность JS. На одном из проектов встретил точно такой же подход в Java, где начинались модных ФП книг и пошли эмулировать монады и каррирование, как итог - еще хуже чем то что я видел в JS.

    Понятно что JS - это не ФП язык. Но не нужно все мерить по Haskell. JS/TS в такой ситуации, что разумная доля ФП подхода без перебора пока что дает сильно больше преимуществ чем другие подходы. Вопрос лишь в адекватных рамках этого.


    1. divideme Автор
      18.04.2026 21:02

      Я очень со многим из того, что вы пишите согласен.

      По мне главные критерии ФП языка - это все-таки неизменяемость из коробки и логика языка в которой программа строиться вокруг функции как основного юнита.

      В одной из книг про Cats Effect читал такое определение:

      ФП - это локальное рассуждение(local reasoning) и композиция, всё остальное есть производные этих двух.

      Вспомнил его, когда читал ваш пост, показалось довольно созвучным.

      Взять Scala, там исключений нет или есть мутабельность по умолчанию? Да там половина языка Java подобна, о чем говорить.

      Согласен, но с одним на мой взгляд важным уточнением: в Scala последние несколько лет укоренилась тенденция создания эффектных систем (Cats Effect, Zio), что очень сильно "качнуло" маятник в сторону чистого функционального программирования.Т.е. да, в языке действительно ярко выраженная мультипарадигмальность, но на практике эта дихотомия сегодня ощутимо менее выражена, если сравнивать с периодом популярности Akka, например.

      Понятно что JS - это не ФП язык. Но не нужно все мерить по Haskell. JS/TS в такой ситуации, что разумная доля ФП подхода без перебора пока что дает сильно больше преимуществ чем другие подходы. Вопрос лишь в адекватных рамках этого.

      Вот это, и то, что вы пишите касательно Haskell-like языков - довольно интересный тейк в том смысле, что ведь действительно в мире программирования есть такие... "Маппинги" вроде "Мы говорим ООП, подразумеваем - Java", мы говорим "FP, подразумеваем - Haskell", но довольно серьезный вопрос в том, насколько корректны подобные "маппинги" в принципе.


  1. fransua
    18.04.2026 21:02

    Недавно завезли немного сахара для итераторов: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/map Через пару лет может и до asynciterator дойдут.


    1. nihil-pro
      18.04.2026 21:02

      Вы из какого года пишете?

      Через пару лет может и до asynciterator дойдут

      Завезли почти десят лет назад

      https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator


      1. orenty7
        18.04.2026 21:02

        Речь не про сами асинхронные итераторы, а про helper method-ы для асинхронных итераторов. То есть про map, filter, reduce, take, и так далее


        1. nihil-pro
          18.04.2026 21:02

          А для чего это может быть нужно? Что с ними делать?


  1. Goodzonchik
    18.04.2026 21:02

    Статья очень интересная, сам хотел буквально пару месяцев назад писать что-то подобное, только про проявления и развитие парадигмы ФП в JS. JS изначально был попыткой перенести функциональный Sheme за 10 дней в браузеры. А после, чтобы другие браузеры могли его использовать завели уже стандарт EcmaScript, но уже как ООП (чтобы новый стандарт приняли благосклонно), ну и в последствии развитие шло в разных направлениях. Что-то добавляли от ФП, например стрелочные (лямбда) функции, что-то от ООП - классы.

    В целом, мультипарадигменность - это как раз и не ФП и не ООП.


  1. ImagineTables
    18.04.2026 21:02

    обрабатывать «исключительные» ситуации без ручных проверок повсюду

    Фундаментальная проблема с исключениями в том, что вы не можете дать программистам механизм обработки ошибок и ожидать, что они не будут им пользоваться. Вот по этой схеме JSON.parse() и бросает исключения, хотя в парсинге json’а нет ничего исключительного, это совершенно нормальный сценарий выполнения.

    Далее повсюду появляются catch’и, потому что они НУЖНЫ всюду, и ситуация «без ручных проверок повсюду» превращается в «ручные проверки повсюду, но вместо универсального if для ошибок используется новый, ничем не обусловленный синтаксис». И хорошо ещё, когда в стандартной библиотеке сделан .TryParse(). Впрочем, он не спасает, а только ещё больше всё запутывает.

    А по теме статьи: берите тех, кто говорит про .map(). Это люди, которые схватывают суть на лету и приносят максимальную пользу. Аналогично, когда на вопрос про ООП люди отвечают «это гарантированный автокомплит после нажатия точки», такой критерий выглядит высокоуровневым и смешноватым, но если человек до него дошёл, он практический философ, который способен выкупить суть и изложить её простыми словами (подумайте сами, как этот критерий задаёт набор низкоуровневых требований). Пользы от такого человека обычно гораздо больше, чем от тех, кто часами трындычит про «три кита — энкапсуляцию, полиморфизм и наследование» (при том, что в некоторых местах я видел строгое полиси не использовать наследование или хотя бы писать объяснительную). Так же и .map() — это выраженная в ёмкой форме гарантия декомпозиции схем преобразования, поддержки встраивания запросов в язык без необходимости таскать DSL и ещё сорока бочек всякого разного. А то, что большие коллекции жрут память — так не надо грузить в браузер большие коллекции (на сервере же с этим мирятся, платя памятью за обобществлённость виртуальной машины, которая никому не принадлежит).


  1. gBear
    18.04.2026 21:02

    При всём уважении - похоже, у вас у самого какая-то “каша в голове” :(

    Имманентный свойством ФЯП (любого - без каких-либо исключений) является исключительно система типов, которая умеет в HOF (а у вас про них вообще нет ничего). Другими словами - без наличия в системе типов функций высшего порядка - ФЯП не бывает.

    А всё остальное - в том числе, и всё вами перечисленное - либо ортогонально, собственно, “функциональности”, и является следствием других категорий ЯП. Либо является следствием наличия HOF в системе типов.

    Ну и - скажем так, на всякий - про “странности”…

    Сама по себе иммутабельность - не есть гарантия ссылочной прозрачности. А ссылочная прозрачность - это про декларативность, а не про функциональность. Имхо, это важно понимать, и различать.

    Аналогично с TCO. Оно важно - исключительно - с точки зрения “энергичной” evaluation strategy. Причем тут функциональность?! Зачем вам TCO, если у вас везде, например, call-by-need?

    “Ленивость” - aka call-by-need - мало того, что “мимо” (в смысле, это не про функциональность), но нужно же понимать, что оно “конфликтует” с TCO. А вы их в один список ставите. Зачем?!

    try/catch — это инструкция, а не выражение, её нельзя композировать …

    Какой-нибудь Erlang смотрит на вас с недоумением :-)

    А то, что вы пишите “про монады” - это - по большому счёту - про наличие/отсутствие, скажем так, “оператора композиции”. И - опять же - к функциональности, как таковой, оно отношения не имеет.

    Про HKT и PM - вы, похоже, не понимаете место этих концепций в ЯП (любом).

    Ну и про “гравитацию”…

    В JS функциональный стиль — это осознанный выбор, который нужно делать каждый раз заново. Default — императивный подход, и он постоянно притягивает к себе.

    Это, имхо, весьма показательно :-)


    1. divideme Автор
      18.04.2026 21:02

      Сама по себе иммутабельность - не есть гарантия ссылочной прозрачности. А ссылочная прозрачность - это про декларативность, а не про функциональность. Имхо, это важно понимать, и различать.

      Я утверждал обратное — что мутабельность по умолчанию ссылочную прозрачность нарушает, а не что иммутабельность её гарантирует, это разные тезисы.

      Про конфликт TCO и ленивости — не очень понял тезис. Haskell ленивый, но TCO там есть и используется. Проблема стека трансформируется в space leaks, но не исчезает совсем. Если имеется в виду что они решают разные проблемы — согласен, но это скорее ортогональность, чем конфликт.

      Касательно try/catch и Erlang: конкретно тут он ни при чём, в том блоке я разбирал try/catch в JS, и там это является инструкцией, а не выражением. И это не моё мнение, а факт.

      Имманентный свойством ФЯП (любого - без каких-либо исключений) является исключительно система типов, которая умеет в HOF (а у вас про них вообще нет ничего). Другими словами - без наличия в системе типов функций высшего порядка - ФЯП не бывает.

      Интересное определение, но оно ломается на примере, который вы сами привели — Erlang динамически типизирован, никакой системы типов с HOF там нет. Тем не менее функциональным языком он считается.

      Если вы имели в виду просто сам факт того, что для ФП необходимо иметь возможность писать HOF - то с этим глупо спорить, всё так. Моя проблема с формулировкой в том, что Если HOF — единственный(или основной) критерий, то JS функциональный язык, it's simple as that.

      Про HKT и PM - вы, похоже, не понимаете место этих концепций в ЯП (любом).

      Звучит как ваше оценочное суждение без каких-либо оснований.


  1. gBear
    18.04.2026 21:02

    Я утверждал обратное — что мутабельность по умолчанию ссылочную прозрачность нарушает, а не что иммутабельность её гарантирует, это разные тезисы.

    Мой поинт-то в том, что не понятно, какое отношение наличие/отсутствие ссылочной прозрачности имеет к функциональности ЯП?

    Haskell ленивый, но TCO там есть и используется.

    Ну если под TCO понимать само наличие call-by-need - то да :-) Но call-by-need - это не TCO. И GR - не TCO. Где вы TCO в haskell нашли? Или - а что вы вообще под TCO тогда понимаете?

    Интересное определение, но оно ломается на примере, который вы сами привели — Erlang динамически типизирован, никакой системы типов с HOF там нет.

    ?! В Erlang замечательная система типов. С чего вы взяли, что её (системы типов) там нет?!

    … то JS функциональный язык, it’s simple as that.

    Ну так и есть. JS функционален ровно в той же степени, в которой функциональна, например, scala. Ибо функциональность - это не про модель вычислений (декларативность/императивность), не про стратегию вычислений (упрощая, ленивость/энергичность) и не про ещё кучу аспектов.

    Функциональность - это про вполне определенное свойство системы типов.

    Другое дело, что можно начать рассуждать о - так сказать - “чистоте” этой функциональности… :-)

    Звучит как ваше оценочное суждение без каких-либо оснований

    Ну потому что, то что вы пишете в этих разделах - “делать мне больно мозг” :-) А “приведение оснований” - это будет долго и оффтопик.

    Про HKT вы там и сами уже пришли к тому, что “оно лишнее”. С моей точки зрения - странным путем, но пришли. И к тому, что PM - из такой же категории - рано или поздно - придете.

    В конце концов, какой-нибудь, common lisp вполне себе живет же без PM на уровне языка :-) Да и само по себе требование exhaustiveness для PM - в разрезе наличия динамической типизации, например - выглядит “странновато”, имхо. Ну или надо будет какое-нибудь Scheme с Clojure и прочими Erlang’ами “лишать звания” :-)


    1. divideme Автор
      18.04.2026 21:02

      Ну так и есть. JS функционален ровно в той же степени, в которой функциональна, например, scala. Ибо функциональность - это не про модель вычислений (декларативность/императивность), не про стратегию вычислений (упрощая, ленивость/энергичность) и не про ещё кучу аспектов.

      Функциональность - это про вполне определенное свойство системы типов.

      Другое дело, что можно начать рассуждать о - так сказать - “чистоте” этой функциональности… :-)

      На мой взгляд вы очень сильно упрощаете. И ваша последняя строчка, где вы как бы "забираете" часть своего аргумента назад - прямое тому подтверждение. JS даже не рядом со Scala в вопросах "функциональности", ни на каком из уровней. Плотность и эргономика функциональных фич в Scala на порядки выше, а систему типов и сравнивать смешно.

      ?! В Erlang замечательная система типов. С чего вы взяли, что её (системы типов) там нет?!

      Вы либо передёргиваете, либо мы изначально говорим о разном. Если вы говорите про спецификацию типов, тогда в вашем первом посту я неверно вас понял.

      Мой поинт-то в том, что не понятно, какое отношение наличие/отсутствие ссылочной прозрачности имеет к функциональности ЯП?

      В узком смысле слова вы правы: СП - это свойство выражения, а не языка. Но проблема в том, что то, что не ссылочно-прозрачно крайне плохо композируется. Если нет функциональной композции и / или она ограниченна, то писать функциональный код становится проблематично и больно.

      Можно долго спорить о терминологии, но факт в том, что культура написания кода на разных языках формируется не только на основании спецификаций и систем типов, но еще и в практическо-прагматической среде. Приживается то, что более естественно для конкретного языка, что более удобно для решения задач, под которые этот язык подходит. И на мой взгляд это куда важнее, чем то, о чём вы говорите выше. Я к тому, что ставить == между языками с точки зрения "функциональности" основываясь только на том, что система типов знает про HOF - это очень сильное упрощение, которое полностью оторванно от реальной практики применения языков: сколько вы лично видели проектов в продакшене на JS, где активно используется карирование, частичное применение, монады, изоляция эффектов(то, что для вас всё это не является критерием фп - и я хочу это подчеркнуть двумя толстыми линиями - это ваше личное оценочное суждение а не научный консенсус)? В то же время чисто функциональную Scala я вижу регулярно, в особенности после роста популярности Cats Effect и Zio


  1. SWATOPLUS
    18.04.2026 21:02

    JS - мультипарадигменный, истинное искусство, правильно применять те или иные подходы, и неизменяемость, и pure-functions. А отсутствие поддержки в языке решается на ревью и линтерами.

    На практике не особо нужно фп в полной мере, а лишь некоторые подходы.


  1. Alexandroppolus
    18.04.2026 21:02

    ТСО не добавляют в js, потому что смысла в этом нет. Если функция подходит для ТСО, то её проще и правильнее переписать на цикл. Тот же "хвостовой" факториал выглядит искусственно в сравнении с обычным: "технический" второй параметр, запутанная логика.


  1. divideme Автор
    18.04.2026 21:02

    удалено


  1. amakhrov
    18.04.2026 21:02

    Ничто не помешает кому-то в другом файле расширить тип

    // Другой файл, другой разработчик, три месяца спустя

    Вообще так не работает. Если у вас есть тип `type Shape` в одном файле, то создание одноименного типа в другом файле (другим разработчиком через 3 месяца) не имеет никакого отношения ни к первому типу, ни к функции, которая тот первый тип использовала. Ремарка про "открытую иерархию" в контексте этого примера совершенно непонятна.


    1. divideme Автор
      18.04.2026 21:02

      Спасибо вам огромное, что заметили эту жесть)) Писал этот кусок глубокой ночью, то ли в каких-то более старых версиях ТS была эта проблема (но даже если и была, то выглядела совсем не так, как я ее описал), то ли я ее сам "выдумал". Весь этот кусок удалил, ваше замечание полностью по делу, спасибо еще раз!