Zpět na domů

JS/TS není funkcionální jazyk: technická analýza

Článek vysvětluje, proč JavaScript a TypeScript nejsou funkcionálními programovacími jazyky. Jsou zváženy čtyři klíčová technická omezení: mutabilita ve výchozím nastavení, absence optimalizace ocasové rekurze, absence líných kolekcí a strukturální dekompozice, stejně jako zpracování chyb prostřednictvím výjimek. Uvedeny příklady a důsledky pro vývoj.

JS/TS není funkcionální jazyk: jak technická omezení ovlivňují váš kód
Advertisement 728x90

JS/TS není funkcionální jazyk: technická omezení a jejich důsledky

Mnoho vývojářů mylně považuje JavaScript a TypeScript za funkcionální jazyky kvůli přítomnosti metod jako .map() a .filter(). Tyto jazyky však nepodporují klíčové principy funkcionálního programování, což vede ke skrytým problémům v produkčním kódu. Probereme čtyři technické důvody, proč JS/TS není funkcionálním jazykem, a jak to ovlivňuje vývoj.

Mutabilita jako výchozí: iluze neměnnosti

Ve funkcionálních jazycích, jako Haskell nebo Clojure, je neměnnost zabudována na úrovni jazyka. Například v Clojure jsou všechny základní datové struktury neměnné hned z krabice. V JavaScript je mutabilita standardní chování. Dokonce i deklarace přes const negarantuje neměnnost dat:

const user = { name: "Alice" };
user.name = "Bob"; // Rabotaet bez oshibok

Tady const chrání pouze před přeassignováním proměnné, ale ne před změnou obsahu objektu. To porušuje referenční průhlednost – klíčové vlastnosti funkcionálního programování, která umožňuje nahradit výraz jeho hodnotou bez změny chování programu.

Google AdInline article slot

Pokusy emulovat neměnnost přes Object.freeze() nebo knihovny jako Immutable.js zůstávají dlahami. Nejsou integrovány do jazyka a vyžadují explicitní rozhodnutí týmu, což snižuje jejich adopci. Důvod takového designu je historický: JavaScript byl vytvořen za 10 dní v roce 1995 jako jednoduchý skriptovací jazyk pro prohlížeč, kde mutabilita zjednodušovala implementaci a odpovídala návykům programátorů z C/Java. Přeprojektovat model paměti bez zničení webu dnes není možné.

Absence optimalizace ocasové rekurze: cena dynamičnosti

Optimalizace ocasové rekurze (TCO) ve standardu ES2015 tak nikdy nedostala podporu v hlavních motorech kromě Safari. V Chrome a Firefox pokus o implementaci hluboké rekurze vede k přetečení zásobníku:

function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}
factorial(100000); // RangeError: Maximum call stack size exceeded

Ve Scala stejná funkce s anotací @tailrec je kompilátorem zaručitě optimalizována na iterativní smyčku, čímž se vyhne přetečení zásobníku. Navíc kompilátor kontroluje možnost optimalizace v fázi kompilace a vydá chybu, pokud ocasový volání není možné.

Google AdInline article slot

Proč V8 nepodporuje TCO? Hlavní důvody:

  • Ztráta stacktrace. TCO nahrazuje aktuální rámec zásobníku, což ztěžuje ladění. Ve Scala kompilátor převádí rekurzi na smyčku a zachovává zásobník volání.
  • Kompatibilita se starými API. Metody jako Function.caller spoléhají na historii volání, kterou TCO vymaže.
  • Složitost v JIT-kompilaci. Víceúrovňová architektura V8 (Ignition → TurboFan) vyžaduje přehodnocení generování a invalidace rámců.

Inženýři V8 otevřeně prohlašují, že náklady na implementaci TCO převyšují přínos pro ekosystém. Toto rozhodnutí odráží priority JS: podpora dynamických možností a zpětná kompatibilita jsou důležitější než přísné záruky funkcionálního programování.

Líné kolekce a strukturální sdílení: neoptimálnost pro velká data

V JavaScript operace nad poli (filter, map) vytvářejí meziprodukty, což vede k špičkové spotřebě paměti:

Google AdInline article slot
const result = hugeArray
  .filter(x => x > 0)   // [1] sozdaetsya array A
  .map(x => x * 2)      // [2] sozdaetsya array B
  .filter(x => x < 100) // [3] sozdaetsya array C
  .slice(0, 10);        // [4] sozdaetsya result

V paměti mohou být současně uloženy hugeArray, A, B, C, i když finální výsledek je malý. Sběrač odpadu pracuje asynchronně, takže špičková zátěž je reálná.

Ve Scala líná zobrazení (.view) převádí řetězec na prvek-po-prvku konvejér bez meziproduktových kolekcí. Navíc perzistentní struktury (jako Vector) využívají structural sharing – při „změně“ se kopíruje pouze cesta od kořene k listu (O(log n)), zbytek se přeutilizuje.

JS takové struktury nepodporuje z krabice. Generátory a nové Iterator Helpers (ES2025) umožňují vytvářet líné řetězce, ale to vyžaduje imperativní kód a není integrováno do standardního API. Důvod – optimalizace pro mainstreamové scénáře:

  • Lokalita dat. Souvislá pole spolupracují s CPU cachem, zatímco líné řetězce generují malé volání.
  • JIT-optimalizace. V8 agresivně inlajnují metody Array.prototype, což nelze udělat pro líné operace.
  • Výkon UI. Pro renderování rozhraní může být kopírování polí rychlejší než dereferencování pointerů ve stromech.

Zpracování chyb: výjimky proti typizovaným výsledkům

V JavaScript jsou chyby výjimky, skryté před systémem typů. Například JSON.parse má signaturu (string) => any, ale může vyhodit výjimku:

function getUser(id: string): User {
  // Maybe brosit isklyuchenie — kompilyator not predupredit
}

Výjimky porušují referenční průhlednost a mění funkci z totální (total) na parciální (partial). Ve funkcionálních jazycích se chyby kódován v typ vracené hodnoty:

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

Tady Try explicitně ukazuje, že operace může skončit neúspěchem, a kompilátor vyžaduje zpracování obou případů. V JS je try/catch instrukce, ne výraz, což přerušuje tok dat a ztěžuje kompozici.

Knihovny jako fp-ts nabízejí obcházky, ale zůstávají externími řešeními, ne zabudovanými do jazyka. To vytváří „slepé zóny“ v typovém systému, kde chyby vznikají neočekávaně.

Co je důležité: klíčové závěry pro vývojáře

  • JS/TS negarantuje neměnnost. Dokonce ani const nechráni před mutací objektů. Pro skutečnou neměnnost jsou potřeba dlahy (Immutable.js), což zvyšuje složitost kódu.
  • Rekurze je v JS nebezpečná. Absence TCO činí rekurzivní algoritmy nevhodnými pro velká data. Vždy kontrolujte hloubku rekurze.
  • Líné operace nejsou nativní. Pro zpracování velkých polí bez špičkové zátěže na paměť budete muset psát imperativní kód nebo používat externí knihovny.
  • Chyby jsou skrytým rizikem. Signatury funkcí neodrážejí možnost výjimek. Vždy obalujte nespolehlivé operace do try/catch, i když TS to nevyžaduje.

— Editorial Team

Advertisement 728x90

Číst dál