3. El Paradigma Funcional
En el Módulo 02 vimos que el paradigma declarativo se enfoca en el «QUÉ» y no en el «CÓMO». La programación funcional es la rama declarativa que construye programas combinando funciones, igual que en matemáticas: los datos entran a una función, salen transformados y nada más se altera en el camino.
- Explicar la idea central del paradigma funcional y de dónde proviene.
- Identificar sus características: funciones puras, inmutabilidad, funciones de primera clase, orden superior, composición y recursión.
- Aplicar
map,filteryreducepara resolver problemas sin bucles manuales. - Evaluar las ventajas y desventajas del paradigma y reconocer dónde se usa hoy en la industria.
🏭 Una línea de producción
Cada estación recibe algo, lo transforma y lo entrega a la siguiente: fruta → lavar → pelar → licuar → jugo. Ninguna estación modifica lo que ocurre en otra, y con la misma fruta siempre se obtiene el mismo jugo. Cada estación es una función.
📐 Función matemática
Si f(x) = x², entonces f(3) vale 9 hoy, mañana y siempre. No depende de la hora, ni de otras variables, ni «cambia» a x. Esa es exactamente la clase de función que busca este paradigma.
Primer contacto: el mismo problema, dos mentalidades
// Duplicar los precios de una lista (estilo imperativo)
const precios = [100, 250, 80];
const duplicados = [];
for (let i = 0; i < precios.length; i++) {
duplicados.push(precios[i] * 2); // modificamos la lista paso a paso
}
console.log(duplicados); // [200, 500, 160]
// El mismo problema (estilo funcional)
const precios = [100, 250, 80];
const duplicar = (x) => x * 2; // 1. una función pequeña
const duplicados = precios.map(duplicar); // 2. se aplica a cada elemento
console.log(duplicados); // [200, 500, 160]
console.log(precios); // [100, 250, 80] <- el original no cambió
Existen lenguajes funcionales «puros» como Haskell, pero JavaScript es multiparadigma: permite escribir tanto de forma imperativa como funcional. Eso nos deja comparar ambos estilos en un lenguaje conocido y aplicar lo aprendido desde hoy en cualquier proyecto web.
El Cálculo Lambda (λ-cálculo)
Un sistema matemático donde todo se expresa con funciones: definirlas y aplicarlas. Se demostró que es tan poderoso como la Máquina de Turing (Tesis de Church-Turing). Es la base teórica de todo el paradigma.
LISP: el primer lenguaje funcional
Llevó el cálculo lambda a una computadora real y fue el lenguaje de la investigación en Inteligencia Artificial por décadas. Aportó ideas que hoy damos por sentadas, como la recolección automática de basura (garbage collector) y el tipado dinámico.
Maduración académica e industrial
ML (1973) introdujo la inferencia de tipos; Erlang (1986, Ericsson) llevó lo funcional a las telecomunicaciones; Haskell (1990) se convirtió en el estándar de lenguaje funcional puro.
Lo funcional llega a los lenguajes populares
Scala (2004), Clojure (2007), F# (2005) y Elixir (2012) nacen funcionales. Los lenguajes masivos adoptan sus ideas: Java 8 (2014) incorpora lambdas y Streams; JavaScript ES6 (2015) agrega funciones flecha; Python, C# y Kotlin hacen lo mismo.
La herencia es directa: la notación λx. x * 2 de Church es, literalmente, lo que hoy escribimos en JavaScript como función flecha.
// Cálculo lambda (1936): λx. x * 2
// JavaScript moderno: x => x * 2
const doble = x => x * 2;
console.log(doble(21)); // 42
Una función es pura cuando cumple dos reglas:
Determinista
Con los mismos argumentos siempre devuelve el mismo resultado. Su salida depende únicamente de sus parámetros.
Sin efectos secundarios
No modifica nada fuera de ella: ni variables globales, ni los objetos que recibe, ni archivos, ni la pantalla. Solo calcula y devuelve.
// ❌ IMPURA 1: depende de una variable externa
let impuesto = 0.15;
function precioFinal(precio) {
return precio + precio * impuesto; // si alguien cambia "impuesto", cambia el resultado
}
// ❌ IMPURA 2: modifica algo fuera de ella (efecto secundario)
let total = 0;
function agregarAlTotal(monto) {
total = total + monto; // altera una variable global
return total;
}
// ❌ IMPURA 3: el resultado cambia en cada llamada
function generarCodigo() {
return Math.floor(Math.random() * 1000);
}
// ✅ PURA: todo lo que necesita llega por parámetros
function precioFinal(precio, impuesto) {
return precio + precio * impuesto;
}
// ✅ PURA: devuelve un valor nuevo, no toca nada externo
function sumar(total, monto) {
return total + monto;
}
console.log(precioFinal(100, 0.15)); // 115 (siempre 115)
console.log(sumar(50, 25)); // 75 (siempre 75)
Modificar una variable global · cambiar un objeto o arreglo recibido por parámetro · imprimir en consola o en pantalla · leer o escribir archivos y bases de datos · hacer peticiones a internet · usar Math.random() o new Date().
Ojo: un programa útil necesita efectos (mostrar resultados, guardar datos). La programación funcional no los elimina: los aísla en pocos lugares bien identificados y mantiene puro todo lo demás.
Transparencia referencial
Como una función pura siempre da el mismo resultado, podemos reemplazar la llamada por su valor sin cambiar el comportamiento del programa. Esto hace el código fácil de razonar, de probar y de optimizar (por ejemplo, guardando resultados en caché).
const cuadrado = x => x * x;
const a = cuadrado(4) + cuadrado(4);
const b = 16 + 16; // podemos reemplazar cuadrado(4) por 16 sin cambiar nada
console.log(a === b); // true
En el paradigma funcional, una vez creado un dato no se cambia. Si necesitamos una versión distinta, construimos una copia nueva con el cambio. Veamos por qué importa con un error muy común:
// ❌ Una función que MUTA el arreglo que recibe
function agregarProducto(carrito, producto) {
carrito.push(producto); // modifica el arreglo original
return carrito;
}
const carritoAna = ["libro"];
const carritoNuevo = agregarProducto(carritoAna, "lápiz");
console.log(carritoNuevo); // ["libro", "lápiz"]
console.log(carritoAna); // ["libro", "lápiz"] 😱 ¡también cambió!
// ✅ Versión inmutable: se crea un arreglo NUEVO
function agregarProducto(carrito, producto) {
return [...carrito, producto]; // operador spread (...) copia y agrega
}
const carritoAna = ["libro"];
const carritoNuevo = agregarProducto(carritoAna, "lápiz");
console.log(carritoNuevo); // ["libro", "lápiz"]
console.log(carritoAna); // ["libro"] ✔ el original está intacto
Lo mismo aplica a los objetos: en lugar de modificar una propiedad, se copia el objeto con el operador spread (...).
const estudiante = { nombre: "Luis", nota: 70 };
// En lugar de modificar: estudiante.nota = 85;
// creamos una copia con el cambio:
const actualizado = { ...estudiante, nota: 85 };
console.log(estudiante); // { nombre: "Luis", nota: 70 }
console.log(actualizado); // { nombre: "Luis", nota: 85 }
| Operación | ❌ Muta el original | ✅ Alternativa inmutable |
|---|---|---|
| Agregar al final | arr.push(x) | [...arr, x] |
| Eliminar elementos | arr.splice(i, 1) | arr.filter(...) |
| Ordenar | arr.sort() | arr.toSorted() o [...arr].sort() |
| Invertir | arr.reverse() | arr.toReversed() |
| Cambiar una propiedad | obj.nota = 85 | { ...obj, nota: 85 } |
const no significa inmutableconst solo impide reasignar la variable, pero el contenido de un arreglo u objeto sí puede cambiar.
const numeros = [1, 2, 3];
numeros.push(4); // ⚠️ esto SÍ funciona: const no protege el contenido
console.log(numeros); // [1, 2, 3, 4]
// Para "congelar" de verdad un objeto o arreglo:
const colores = Object.freeze(["rojo", "verde"]);
// colores.push("azul"); -> TypeError: no se puede agregar la propiedad
- Menos errores sorpresa: nadie puede cambiar «a escondidas» un dato que otra parte del programa está usando.
- Historial gratuito: como las versiones anteriores no se destruyen, es fácil implementar «deshacer» o comparar estados.
- Concurrencia segura: varios hilos o procesadores pueden leer los mismos datos sin bloqueos, porque nadie los modifica.
Antes de continuar, un repaso rápido de la sintaxis de funciones flecha (arrow functions), que usaremos en el resto del módulo:
// Función tradicional
function sumar(a, b) {
return a + b;
}
// La misma función escrita como función flecha (arrow function)
const sumarFlecha = (a, b) => a + b;
// Con un solo parámetro los paréntesis son opcionales
const cuadrado = x => x * x;
console.log(sumar(2, 3), sumarFlecha(2, 3), cuadrado(5)); // 5 5 25
Funciones como ciudadanos de primera clase
Una función puede hacer todo lo que hace cualquier otro valor: guardarse en una variable, pasarse como argumento, devolverse como resultado o almacenarse en un arreglo u objeto.
Funciones de orden superior
Son funciones que reciben otra función como parámetro o que devuelven una función. map, filter y reduce son los ejemplos más famosos.
// 1. Guardar una función en una variable
const saludar = nombre => "Hola, " + nombre;
// 2. Pasar una función como argumento
function aplicarDosVeces(fn, valor) {
return fn(fn(valor));
}
const sumarDiez = x => x + 10;
console.log(aplicarDosVeces(sumarDiez, 5)); // 25
// 3. Devolver una función como resultado
function crearMultiplicador(factor) {
return x => x * factor; // la función "recuerda" el factor (closure)
}
const triple = crearMultiplicador(3);
console.log(triple(7)); // 21
// 4. Guardar funciones dentro de estructuras de datos
const operaciones = [x => x + 1, x => x * 2];
console.log(operaciones[1](8)); // 16
En crearMultiplicador, la función devuelta «recuerda» el valor de factor aunque crearMultiplicador ya terminó de ejecutarse. A esa memoria se le llama closure y permite fabricar funciones especializadas a partir de una general.
Estas tres funciones de orden superior reemplazan la gran mayoría de los ciclos for. Trabajaremos con estos datos en todos los ejemplos:
const estudiantes = [
{ nombre: "Ana", nota: 85 },
{ nombre: "Luis", nota: 58 },
{ nombre: "María", nota: 92 },
{ nombre: "Pedro", nota: 64 },
{ nombre: "Sofía", nota: 77 },
];
map
Aplica una función a cada elemento y devuelve un arreglo nuevo del mismo tamaño.
[🍎, 🍎, 🍎] → [🍏, 🍏, 🍏]
filter
Conserva solo los elementos que cumplen una condición (la función devuelve true o false).
[🍎, 🍌, 🍎] → [🍎, 🍎]
reduce
Combina todos los elementos, uno a uno, hasta obtener un solo valor: una suma, un máximo, un objeto…
[1, 2, 3] → 6
// map: transforma CADA elemento -> devuelve un arreglo del mismo tamaño
const nombres = estudiantes.map(e => e.nombre);
console.log(nombres); // ["Ana", "Luis", "María", "Pedro", "Sofía"]
const curva = estudiantes.map(e => ({ ...e, nota: e.nota + 5 }));
console.log(curva[1]); // { nombre: "Luis", nota: 63 }
// filter: se queda SOLO con los que cumplen la condición
const aprobados = estudiantes.filter(e => e.nota >= 65);
console.log(aprobados.length); // 3 (Ana, María y Sofía)
// reduce: combina todos los elementos en UN solo valor
// (acumulado, actual) => nuevo acumulado, valor inicial
const suma = estudiantes.reduce((acc, e) => acc + e.nota, 0);
const promedio = suma / estudiantes.length;
console.log(promedio); // 75.2
Cara a cara: promedio de los estudiantes aprobados
// Promedio de los aprobados (imperativo)
let suma = 0;
let cantidad = 0;
for (let i = 0; i < estudiantes.length; i++) {
if (estudiantes[i].nota >= 65) {
suma += estudiantes[i].nota;
cantidad++;
}
}
const promedio = suma / cantidad;
console.log(promedio.toFixed(2)); // "84.67"
// Promedio de los aprobados (funcional)
const notas = estudiantes
.filter(e => e.nota >= 65) // ¿quiénes?
.map(e => e.nota); // ¿qué dato?
const promedio = notas.reduce((acc, n) => acc + n, 0) / notas.length;
console.log(promedio.toFixed(2)); // "84.67"
La versión funcional se lee casi como la especificación del problema: «de los estudiantes, filtra los aprobados, toma sus notas y promédialas». No hay contadores, índices ni variables que cambien de valor.
find (primer elemento que cumple) · some (¿alguno cumple?) · every (¿todos cumplen?) · forEach (recorre, pero solo para efectos como imprimir; no devuelve nada).
En matemáticas, componer es aplicar una función al resultado de otra: (f ∘ g)(x) = f(g(x)). En programación funcional, se resuelven problemas complejos armando una tubería (pipeline) de funciones simples, como las estaciones de la línea de producción.
Forma 1: encadenamiento de métodos
// Lista de honor: aprobados, ordenados por nota, solo el nombre en mayúsculas
const listaHonor = estudiantes
.filter(e => e.nota >= 65) // 1. quedarse con los aprobados
.toSorted((a, b) => b.nota - a.nota) // 2. ordenar SIN mutar el original
.map(e => e.nombre.toUpperCase()); // 3. transformar a texto
console.log(listaHonor); // ["MARÍA", "ANA", "SOFÍA"]
Forma 2: composición con pipe
Cuando las funciones no son métodos de un arreglo, podemos crear una utilidad pipe que las ejecute en orden, pasando el resultado de una a la siguiente:
// Funciones pequeñas, cada una hace UNA sola cosa
const quitarEspacios = texto => texto.trim();
const aMinusculas = texto => texto.toLowerCase();
const cambiarEspacios = texto => texto.replaceAll(" ", "-");
// pipe: ejecuta las funciones de izquierda a derecha
const pipe = (...fns) => valor => fns.reduce((acc, fn) => fn(acc), valor);
const crearSlug = pipe(quitarEspacios, aMinusculas, cambiarEspacios);
console.log(crearSlug(" Paradigmas de Programación ")); // "paradigmas-de-programación"
" Paradigmas de Programación " → quitarEspacios → aMinusculas → cambiarEspacios → "paradigmas-de-programación"
Cada pieza es pequeña, pura y se puede probar por separado. Para cambiar el comportamiento basta con agregar, quitar o reordenar piezas.
Los bucles for y while dependen de un contador que cambia (i++), lo cual choca con la inmutabilidad. Los lenguajes funcionales puros ni siquiera tienen bucles: repiten mediante recursión. Toda función recursiva tiene dos partes:
🛑 Caso base
La condición en la que la función deja de llamarse y devuelve un resultado directo. Sin él, la recursión nunca termina.
🔁 Paso recursivo
La función se llama a sí misma con un problema más pequeño, acercándose cada vez al caso base.
// Con bucle (imperativo)
function cuentaRegresivaFor(n) {
for (let i = n; i > 0; i--) {
console.log(i);
}
console.log("¡Despegue!");
}
// Con recursión (funcional)
function cuentaRegresiva(n) {
if (n === 0) { // 1. CASO BASE: cuándo detenerse
console.log("¡Despegue!");
return;
}
console.log(n);
cuentaRegresiva(n - 1); // 2. PASO RECURSIVO: un problema más pequeño
}
cuentaRegresiva(3); // 3, 2, 1, ¡Despegue!
// factorial(4) = 4 * 3 * 2 * 1 = 24
const factorial = n =>
n === 0 ? 1 // caso base
: n * factorial(n - 1); // paso recursivo
console.log(factorial(4)); // 24
/* ¿Cómo se evalúa?
factorial(4)
= 4 * factorial(3)
= 4 * 3 * factorial(2)
= 4 * 3 * 2 * factorial(1)
= 4 * 3 * 2 * 1 * factorial(0)
= 4 * 3 * 2 * 1 * 1 = 24 */
// Sumar una lista separándola en "primero" y "resto"
function sumarLista(lista) {
if (lista.length === 0) return 0; // caso base: lista vacía
const [primero, ...resto] = lista; // desestructuración
return primero + sumarLista(resto); // primero + suma del resto
}
console.log(sumarLista([4, 7, 1, 3])); // 15
// 4 + sumarLista([7,1,3]) -> 4 + 7 + sumarLista([1,3]) -> ... -> 15
Cada llamada ocupa espacio en la pila de llamadas (call stack). Con listas muy grandes (decenas de miles de elementos) JavaScript lanza el error Maximum call stack size exceeded. Por eso, en JavaScript lo habitual es usar map, filter y reduce para recorrer colecciones, y reservar la recursión para problemas naturalmente recursivos: árboles, carpetas, menús anidados o JSON con niveles.
- Código predecible: las funciones puras siempre responden igual, por lo que hay menos errores «misteriosos».
- Fácil de probar: para testear una función pura basta con darle entradas y revisar la salida; no hay que preparar estado global.
- Reutilizable y modular: funciones pequeñas que se combinan como piezas de LEGO.
- Concurrencia y paralelismo más seguros: sin datos compartidos que cambien, no hay condiciones de carrera.
- Código más expresivo y corto: se describe qué se quiere, no cada paso.
- Curva de aprendizaje: exige cambiar la forma de pensar si venimos del estilo imperativo.
- Consumo de memoria: crear copias en lugar de modificar puede costar más memoria y tiempo.
- Recursión profunda: en lenguajes como JavaScript puede desbordar la pila.
- El mundo real tiene estado: bases de datos, interfaces y archivos obligan a manejar efectos con cuidado.
- Abuso de la abstracción: cadenas de funciones muy largas o «demasiado ingeniosas» pueden volverse difíciles de leer.
| Criterio | Imperativo | Funcional |
|---|---|---|
| Unidad principal | Instrucciones que se ejecutan en orden | Funciones que transforman datos |
| Datos | Mutables (x = x + 1) | Inmutables (se crean copias) |
| Repetición | for, while | map, filter, reduce, recursión |
| Estado | Compartido y cambiante | Se evita o se aísla |
| Ideal para | Control fino del hardware, algoritmos de bajo nivel, rendimiento extremo | Transformación de datos, lógica de negocio, sistemas concurrentes |
Haskell
No permite variables mutables ni efectos secundarios sin declararlos. Muy usado en investigación, compiladores y finanzas.
Clojure, Elixir, Erlang, F#, Scala
Promueven funciones puras e inmutabilidad por defecto, pero permiten efectos cuando hace falta.
JavaScript, Python, Java, C#, Kotlin
Nacieron imperativos u orientados a objetos, pero hoy incorporan lambdas, map/filter/reduce y otras herramientas funcionales.
¿Dónde lo usarán?
- Desarrollo web con React: los componentes son funciones que reciben datos y devuelven la interfaz, y el estado se actualiza de forma inmutable.
- Big Data: el modelo MapReduce de Google y herramientas como Apache Spark se basan en las mismas ideas de
mapyreducepara procesar datos en miles de servidores en paralelo. - Sistemas de alta disponibilidad: WhatsApp construyó su servidor de mensajería sobre Erlang, aprovechando la concurrencia sin estado compartido.
- Java moderno: la Stream API que vimos en el Módulo 02 es programación funcional dentro de Java.
// En React, un componente es simplemente una función:
// recibe datos (props) y devuelve lo que se debe mostrar.
function Saludo({ nombre }) {
return <h1>Hola, {nombre}</h1>;
}
// Y el estado se actualiza de forma inmutable:
setTareas(tareas => [...tareas, nuevaTarea]);
Currificación (currying): transformar una función de varios parámetros en una cadena de funciones de un solo parámetro.
// Currificación: una función de varios parámetros
// se convierte en una cadena de funciones de un parámetro
const sumar = a => b => a + b;
const sumarCinco = sumar(5); // función "a medio aplicar"
console.log(sumarCinco(10)); // 15
console.log(sumar(2)(3)); // 5
Evaluación perezosa (lazy evaluation): una expresión no se calcula hasta que su valor realmente se necesita; Haskell la usa por defecto y permite trabajar con listas «infinitas».
Mónadas: el mecanismo que usan los lenguajes puros, como Haskell, para manejar efectos (leer teclado, escribir archivos) sin romper la pureza.
// ¿Cuál de estas funciones es PURA y cuál es IMPURA?
// A)
const areaCirculo = r => 3.1416 * r * r;
// B)
let contador = 0;
const incrementar = () => ++contador;
// C)
const saludar = nombre => console.log("Hola " + nombre);
// D)
const mayorDeEdad = persona => persona.edad >= 18;
Ver solución
r y no modifica nada.B) Impura: modifica la variable externa
contador y su resultado cambia en cada llamada.C) Impura: imprimir en consola es un efecto secundario (además no devuelve nada).
D) Pura: solo lee la propiedad recibida y devuelve
true o false.
// Reescribe este código usando filter, map y reduce
const productos = [
{ nombre: "Teclado", precio: 450, stock: 3 },
{ nombre: "Mouse", precio: 200, stock: 0 },
{ nombre: "Monitor", precio: 3200, stock: 2 },
];
let totalInventario = 0;
for (let i = 0; i < productos.length; i++) {
if (productos[i].stock > 0) {
totalInventario += productos[i].precio * productos[i].stock;
}
}
console.log(totalInventario); // 7750
Ver solución
const totalInventario = productos
.filter(p => p.stock > 0) // solo los que tienen existencias
.map(p => p.precio * p.stock) // valor de cada producto
.reduce((acc, valor) => acc + valor, 0); // sumar todo
console.log(totalInventario); // 7750
// Escribe una función RECURSIVA que encuentre el número mayor de una lista
// maximo([3, 9, 2, 7]) -> 9
Ver pista
Ver solución
function maximo(lista) {
if (lista.length === 1) return lista[0]; // caso base
const [primero, ...resto] = lista;
const maxResto = maximo(resto); // resolver el problema más pequeño
return primero > maxResto ? primero : maxResto;
}
console.log(maximo([3, 9, 2, 7])); // 9
Ver explicación
const a = [1, 2], ¿qué instrucción respeta la inmutabilidad?
Inmutabilidad
a.push(3)a[0] = 10const b = [...a, 3]Ver explicación
push y la asignación por índice modifican el arreglo original. El operador spread crea un arreglo nuevo y deja a intacto.[3, 8, 5].filter(n => n > 4)?
Orden superior
[8, 5][true, false, true]13Ver explicación
filter conserva los elementos para los que la función devuelve true. Devolver booleanos para cada elemento sería lo que hace map con esa misma función.undefined automáticamente.for.Ver explicación
Maximum call stack size exceeded.Ver explicación
Programar funcionalmente es transformar datos con funciones puras, sin modificar lo existente. Las funciones son valores que se pasan y se combinan; map, filter y reduce reemplazan a los bucles; y la recursión resuelve problemas dividiéndolos en versiones más pequeñas. El resultado es código más predecible y fácil de probar, a cambio de una nueva forma de pensar y cierto costo de memoria.