Volver al inicio

JS/TS no es un lenguaje funcional: análisis técnico

El artículo explica por qué JavaScript y TypeScript no son lenguajes de programación funcional. Se consideran cuatro limitaciones técnicas clave: mutabilidad por defecto, falta de optimización de recursión de cola, falta de colecciones perezosas y coincidencia de patrones estructural, así como manejo de errores a través de excepciones. Se proporcionan ejemplos y consecuencias para el desarrollo.

JS/TS no es un lenguaje funcional: cómo las limitaciones técnicas afectan tu código
Advertisement 728x90

JS/TS no es un lenguaje funcional: Limitaciones técnicas y sus consecuencias

Muchos desarrolladores piensan erróneamente que JavaScript y TypeScript son lenguajes funcionales por métodos como .map() y .filter(). Sin embargo, estos lenguajes no soportan principios clave de la programación funcional, lo que lleva a bugs ocultos en código de producción. Vamos a desglosar cuatro razones técnicas por las que JS/TS no es un lenguaje funcional y cómo eso impacta el desarrollo.

Mutabilidad por defecto: La ilusión de la inmutabilidad

En lenguajes funcionales como Haskell o Clojure, la inmutabilidad está integrada en el lenguaje. Por ejemplo, en Clojure, todas las estructuras de datos básicas son inmutables de fábrica. En JavaScript, en cambio, la mutabilidad es el comportamiento por defecto. Incluso declarar con const no garantiza la inmutabilidad:

const user = { name: "Alice" };
user.name = "Bob"; // Works without errors

Aquí, const solo impide reasignar la variable, no mutar el contenido del objeto. Esto rompe la transparencia referencial, una propiedad central de la programación funcional que permite reemplazar una expresión por su valor sin cambiar el comportamiento del programa.

Google AdInline article slot

Los intentos de emular inmutabilidad con Object.freeze() o librerías como Immutable.js son solo parches. No están integrados en el lenguaje y requieren decisiones explícitas del equipo, lo que limita su adopción. La razón de este diseño es histórica: JavaScript se creó en 10 días en 1995 como un lenguaje de guion simple para navegadores, donde la mutabilidad simplificaba la implementación y coincidía con los hábitos de programadores de C/Java. Rediseñar el modelo de memoria hoy sin romper la web es imposible.

Sin optimización de llamadas en cola: El costo del dinamismo

La optimización de llamadas en cola (TCO), propuesta en el estándar ES2015, aún no está soportada en los motores principales salvo Safari. En Chrome y Firefox, una recursión profunda provocará un desbordamiento de pila:

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

En Scala, la misma función con la anotación @tailrec está garantizada de ser optimizada por el compilador en un bucle iterativo, evitando el desbordamiento de pila. El compilador incluso verifica la viabilidad de la optimización en tiempo de compilación, lanzando un error si las llamadas en cola no son posibles.

Google AdInline article slot

¿Por qué V8 no soporta TCO? Razones principales:

  • Pérdida de trazas de pila. TCO reemplaza el marco de pila actual, lo que complica la depuración. En Scala, el compilador convierte la recursión en un bucle preservando la pila de llamadas.
  • Compatibilidad hacia atrás con APIs legacy. Métodos como Function.caller dependen del historial de llamadas, que TCO elimina.
  • Complejidad en la compilación JIT. La arquitectura multi-etapa de V8 (Ignition → TurboFan) requeriría repensar la generación e invalidación de marcos.

Los ingenieros de V8 afirman abiertamente que el costo de implementar TCO supera los beneficios para el ecosistema. Esta elección refleja las prioridades de JS: las características dinámicas y la compatibilidad hacia atrás priman sobre garantías funcionales estrictas.

Colecciones perezosas y compartición estructural: Subóptimo para big data

En JavaScript, las operaciones de array (filter, map) crean copias intermedias, disparando el uso de memoria:

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

Todas estas pueden coexistir en memoria —hugeArray, A, B, C— incluso si el resultado final es minúsculo. El recolector de basura se ejecuta de forma asíncrona, por lo que la carga máxima es real.

En Scala, las vistas perezosas (.view) convierten la cadena en un pipeline bajo demanda sin colecciones intermedias. Además, estructuras persistentes como Vector usan compartición estructural: solo se copia el camino de la raíz a la hoja en un "cambio" (O(log n)), reutilizando el resto.

JS no soporta esto de forma nativa. Los generadores y los nuevos Iterator Helpers (ES2025) permiten cadenas perezosas, pero requieren código imperativo y no forman parte de la API estándar. ¿La razón? Optimización para casos de uso mayoritarios:

  • Localidad de datos. Los arrays contiguos funcionan bien con las cachés de CPU, mientras que las cadenas perezosas generan llamadas minúsculas.
  • Optimizaciones JIT. V8 inlinea agresivamente los métodos de Array.prototype, algo que las operaciones perezosas impiden.
  • Rendimiento de UI. Para renderizar interfaces, copiar arrays puede ser más rápido que perseguir punteros en árboles.

Manejo de errores: Excepciones vs. Resultados tipados

En JavaScript, los errores son excepciones ocultas al sistema de tipos. Por ejemplo, JSON.parse tiene signatura (string) => any, pero puede lanzar una excepción:

function getUser(id: string): User {
  // Can throw an exception—compiler won't warn
}

Las excepciones rompen la transparencia referencial y convierten funciones totales en parciales. Los lenguajes funcionales codifican los errores en el tipo de retorno:

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

Aquí, Try señala explícitamente un posible fallo, y el compilador exige manejar ambos casos. En JS, try/catch es una declaración, no una expresión, lo que rompe el flujo de datos y complica la composición.

Librerías como fp-ts ofrecen soluciones alternativas, pero son externas, no integradas en el lenguaje. Esto crea puntos ciegos en el sistema de tipos donde los errores surgen de forma inesperada.

Lecciones clave para desarrolladores

  • JS/TS no garantiza la inmutabilidad. Ni siquiera const protege contra la mutación de objetos. La inmutabilidad real requiere parches (Immutable.js), lo que añade complejidad al código.
  • La recursión es riesgosa en JS. Sin TCO, los algoritmos recursivos no son aptos para big data. Siempre verifica la profundidad de recursión.
  • Las operaciones perezosas no son nativas. Manejar arrays grandes sin picos de memoria requiere código imperativo o librerías de terceros.
  • Los errores son riesgos ocultos. Las signaturas de funciones no reflejan excepciones. Siempre envuelve operaciones no confiables en try/catch, aunque TS no lo exija.

— Editorial Team

Advertisement 728x90

Leer después