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.
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é.
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.callerspolé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:
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
constnechrá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
Zatím žádné komentáře.