Funciones puras y efectos secundarios

Intermedio
TypeScript
TypeScript
Actualizado: 09/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

Definición de funciones puras y su importancia

Una función pura es un concepto fundamental en programación funcional que cumple dos reglas básicas:

  1. Determinismo: Para los mismos argumentos de entrada, siempre devuelve el mismo resultado, sin importar cuándo o dónde se ejecute.
  2. Sin efectos secundarios: No modifica ningún estado fuera de su ámbito, ni depende de estados externos que puedan cambiar.

Una manera sencilla de entenderlo: una función pura sólo depende de sus parámetros de entrada para calcular su resultado y no modifica nada fuera de sí misma.

// Ejemplo de función pura
function add(a: number, b: number): number {
  return a + b;
}

// Siempre devuelve 8 para los mismos parámetros
console.log(add(5, 3)); // 8
console.log(add(5, 3)); // 8 (siempre el mismo resultado)

Características de las funciones puras

Las funciones puras tienen tres propiedades esenciales:

// 1. Determinismo: mismas entradas => mismo resultado
function calculateCircleArea(radius: number): number {
  return Math.PI * radius * radius;
}

// 2. Sin efectos secundarios: no modifica variables externas
function formatName(firstName: string, lastName: string): string {
  return `${firstName} ${lastName}`;
}

// 3. Transparencia referencial: podemos sustituir la llamada por su resultado
const radius = 5;
const area = calculateCircleArea(radius); // Podríamos reemplazar esto por el valor 78.54...

Relación con la inmutabilidad

Las funciones puras están estrechamente relacionadas con la inmutabilidad que estudiamos en la lección anterior. En lugar de modificar datos existentes, crean y devuelven nuevos valores

// Función pura que trabaja con arrays de forma inmutable
function addItem<T>(array: T[], item: T): T[] {
  // Creamos un nuevo array en lugar de modificar el original con push()
  return [...array, item];
}

const numbers = [1, 2, 3];
const newNumbers = addItem(numbers, 4);

console.log(numbers);     // [1, 2, 3] - el original no cambia
console.log(newNumbers);  // [1, 2, 3, 4] - nuevo array con el elemento añadido

Beneficios de las funciones puras

Las funciones puras ofrecen ventajas significativas que mejoran la calidad del código:

  • Facilidad de prueba: Al no depender del contexto externo, son más fáciles de probar unitariamente.
// Fácil de probar: solo necesitamos verificar que 2+3=5
test('add function correctly adds two numbers', () => {
  expect(add(2, 3)).toBe(5);
});
  • Predictibilidad: Su comportamiento es consistente y sin sorpresas.
  • Seguridad para concurrencia: Al no compartir estado mutable, son seguras en entornos multihilo.
  • Razonamiento simplificado: Podemos entender una función analizando solo su código, sin preocuparnos por el estado global.
  • Componibilidad: Las funciones puras se pueden combinar fácilmente para crear funcionalidades más complejas.
// Podemos combinar funciones puras fácilmente
const double = (x: number): number => x * 2;
const increment = (x: number): number => x + 1;

// Componemos las funciones
const doubleAndIncrement = (x: number): number => increment(double(x));

console.log(doubleAndIncrement(3)); // 7 (primero duplica: 6, luego incrementa: 7)

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Identificación y manejo de efectos secundarios

¿Que son los efectos secundarios?

Los efectos secundarios son cambios observables que ocurren fuera del ámbito de una función. Una función tiene efectos secundarios cuando:

// Modifica variables globales
let counter = 0;
function incrementCounter(): number {
  counter++; // Efecto secundario: modifica una variable externa
  return counter;
}

// Interactúa con el sistema de archivos, red o APIs
function fetchUserData(userId: string): Promise<any> {
  return fetch(`https://api.example.com/users/${userId}`); // Efecto secundario: llamada a API
}

// Modifica el DOM o la interfaz de usuario
function updateUI(message: string): void {
  document.getElementById('status')!.textContent = message; // Efecto secundario: modifica el DOM
}

// Interactúa con servicios del sistema
function saveToLocalStorage(key: string, data: any): void {
  localStorage.setItem(key, JSON.stringify(data)); // Efecto secundario: modifica el almacenamiento
}

Tipos comunes de efectos secundarios

Los efectos secundarios más frecuentes en aplicaciones TypeScript incluyen:

  • Modificación de variables fuera del ámbito de la función
  • Operaciones de entrada/salida (E/S)
  • Manipulación del DOM o la interfaz de usuario
  • Llamadas a API o servicios externos
  • Uso de funciones no deterministas (como Math.random() o Date.now())

Cómo identificar efectos secundarios

Existen señales que indican la presencia de efectos secundarios en el código:

// 1. Funciones que no retornan valores (tipo void)
function logMessage(message: string): void {
  console.log(message); // Efecto secundario: escritura en la consola
}

// 2. Acceso a variables fuera del ámbito de la función
let globalConfig = { apiUrl: 'https://api.example.com' };
function getApiData(): Promise<any> {
  return fetch(globalConfig.apiUrl); // Efecto secundario: depende de estado global
}

// 3. Nombres de funciones que sugieren acciones en lugar de transformaciones
// saveUser(), updateStatus(), deleteRecord(), sendEmail() suelen indicar efectos secundarios

Efectos secundarios necesarios

No todos los efectos secundarios son "malos" - muchos son necesarios para que nuestras aplicaciones sean útiles. Lo importante es:

  • Identificarlos claramente
  • Aislarlos del resto del código
  • Manejarlos de forma controlada
// Ejemplo de aislamiento de efectos secundarios
// Separamos la lógica pura de los efectos secundarios

// Parte pura: cálculo del total
function calculateOrderTotal(items: Array<{price: number; quantity: number}>): number {
  return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}

// Efecto secundario aislado: guardado en base de datos
function saveOrderToDatabase(order: Order): Promise<void> {
  return database.orders.save(order);
}

// Función que orquesta ambas partes
async function processOrder(order: Order): Promise<Order> {
  // Primero calculamos el total (operación pura)
  const total = calculateOrderTotal(order.items);
  
  // Creamos una nueva orden con el total (operación pura)
  const completedOrder = { ...order, total };
  
  // Finalmente realizamos el efecto secundario
  await saveOrderToDatabase(completedOrder);
  
  return completedOrder;
}

Técnicas para transformar funciones impuras en puras

Principio básico: separación de responsabilidades

La técnica fundamental es separar la lógica pura (cálculos, transformaciones) de los efectos secundarios (E/S, modificaciones de estado):

// Ejemplo de aislamiento de efectos secundarios
// Separamos la lógica pura de los efectos secundarios

// Parte pura: cálculo del total
function calculateOrderTotal(items: Array<{price: number; quantity: number}>): number {
  return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}

// Efecto secundario aislado: guardado en base de datos
function saveOrderToDatabase(order: Order): Promise<void> {
  return database.orders.save(order);
}

// Función que orquesta ambas partes
async function processOrder(order: Order): Promise<Order> {
  // Primero calculamos el total (operación pura)
  const total = calculateOrderTotal(order.items);
  
  // Creamos una nueva orden con el total (operación pura)
  const completedOrder = { ...order, total };
  
  // Finalmente realizamos el efecto secundario
  await saveOrderToDatabase(completedOrder);
  
  return completedOrder;
}

Uso de parámetros en lugar de estado global

Podemos transformar funciones impuras que dependen de estado global pasando ese estado como parámetros:

// Versión impura: depende de una variable global
let apiUrl = 'https://api.example.com';
function fetchData(endpoint: string): Promise<any> {
  return fetch(`${apiUrl}/${endpoint}`);
}

// Versión pura: recibe la URL como parámetro
function fetchData(apiUrl: string, endpoint: string): Promise<any> {
  return fetch(`${apiUrl}/${endpoint}`);
}

Inyección de dependencias

Para servicios externos o funcionalidades con efectos secundarios, podemos inyectarlos como parámetros:

// Versión impura: dependencia directa del servicio
function getUserData(userId: string): User {
  // Dependencia directa de la implementación
  return databaseService.getUser(userId);
}

// Versión mejorada: inyección de dependencias
interface UserRepository {
  getUser(id: string): User;
}

function getUserData(userRepo: UserRepository, userId: string): User {
  return userRepo.getUser(userId);
}

// Ahora podemos pasar diferentes implementaciones:
// - Implementación real para producción
// - Implementación mock para testing

Transformación de operaciones sobre arrays

Las operaciones mutables sobre arrays son comunes. Podemos transformarlas en versiones inmutables:

// Operaciones impuras sobre arrays
function addItemImpure(cart: string[]): void {
  cart.push("New Item"); // Muta el array original
}

function removeItemImpure(cart: string[], index: number): void {
  cart.splice(index, 1); // Muta el array original
}

// Versiones puras e inmutables
function addItem(cart: string[]): string[] {
  return [...cart, "New Item"]; // Crea un nuevo array
}

function removeItem(cart: string[], index: number): string[] {
  return [...cart.slice(0, index), ...cart.slice(index + 1)]; // Crea un nuevo array
}

// Versiones puras usando métodos funcionales
function filterExpensiveItems(products: Product[]): Product[] {
  return products.filter(product => product.price > 100);
}

function applyDiscount(products: Product[]): Product[] {
  return products.map(product => ({
    ...product,
    price: product.price * 0.9 // 10% de descuento
  }));
}

Patrones para objetos inmutables

Podemos aplicar actualizaciones inmutables a objetos para mantener la pureza:

// Operación impura sobre objeto
function updateUserAgeImpure(user: User): void {
  user.age += 1; // Modifica el objeto original
}

// Versión pura con spread operator
function updateUserAge(user: User): User {
  return { ...user, age: user.age + 1 }; // Crea un nuevo objeto
}

// Actualización de propiedades anidadas
function updateUserAddress(
  user: User, 
  newCity: string
): User {
  return {
    ...user,
    address: {
      ...user.address,
      city: newCity
    }
  };
}

3.6 Aplicando estas técnicas a un ejemplo completo

Veamos una refactorización completa de una función impura, aplicando los principios que hemos visto:

// Versión original: función impura con múltiples efectos secundarios
function processUserData(userId: string): void {
  // Efecto secundario: obtiene datos del almacenamiento
  const userData = localStorage.getItem(`user_${userId}`);
  const user = userData ? JSON.parse(userData) : { id: userId, visits: 0 };
  
  // Modifica el objeto directamente
  user.visits++;
  user.lastVisit = new Date().toISOString();
  
  // Múltiples efectos secundarios
  localStorage.setItem(`user_${userId}`, JSON.stringify(user));
  document.getElementById('userName')!.textContent = user.name || 'Guest';
  
  if (user.visits > 10) {
    // Otro efecto secundario
    sendAnalytics('frequent_user', userId);
  }
}

// Versión refactorizada: separando la lógica pura de los efectos secundarios
interface User {
  id: string;
  name?: string;
  visits: number;
  lastVisit?: string;
}

// 1. Funciones puras
function createDefaultUser(userId: string): User {
  return { id: userId, visits: 0 };
}

function updateUserStats(user: User): User {
  return {
    ...user,
    visits: user.visits + 1,
    lastVisit: new Date().toISOString()
  };
}

function isFrequentUser(user: User): boolean {
  return user.visits > 10;
}

// 2. Funciones con efectos secundarios aislados
function getUserFromStorage(userId: string): User {
  const userData = localStorage.getItem(`user_${userId}`);
  return userData ? JSON.parse(userData) : createDefaultUser(userId);
}

function saveUserToStorage(user: User): void {
  localStorage.setItem(`user_${user.id}`, JSON.stringify(user));
}

function updateUserUI(user: User): void {
  document.getElementById('userName')!.textContent = user.name || 'Guest';
}

function trackUserAnalytics(userId: string, eventName: string): void {
  sendAnalytics(eventName, userId);
}

// 3. Función orquestadora que compone las anteriores
function processUserData(userId: string): User {
  // Obtenemos el usuario (efecto secundario)
  const user = getUserFromStorage(userId);
  
  // Actualizamos estadísticas (operación pura)
  const updatedUser = updateUserStats(user);
  
  // Guardamos los datos (efecto secundario)
  saveUserToStorage(updatedUser);
  
  // Actualizamos UI (efecto secundario)
  updateUserUI(updatedUser);
  
  // Comprobamos y enviamos analíticas si es necesario (efecto secundario condicional)
  if (isFrequentUser(updatedUser)) {
    trackUserAnalytics(userId, 'frequent_user');
  }
  
  return updatedUser;
}

En este ejemplo refactorizado:

  • Cada función tiene una única responsabilidad
  • Las funciones puras realizan cálculos y transformaciones
  • Los efectos secundarios están aislados en funciones específicas
  • La función principal orquesta todo el proceso

Conclusión

Las funciones puras son una herramienta poderosa que nos permite escribir código más predecible, testeable y mantenible. Al identificar y aislar los efectos secundarios, podemos razonar mejor sobre nuestro código y prevenir errores difíciles de detectar.

En la práctica, no todo nuestro código puede ser 100% puro, pero podemos aplicar estos principios para organizar nuestro código en componentes bien definidos donde la lógica pura está separada de los efectos secundarios necesarios.

La combinación de inmutabilidad (vista en la lección anterior) y funciones puras nos proporciona una base sólida para aplicar principios de programación funcional en nuestras aplicaciones TypeScript, resultando en sistemas más robustos y mantenibles.

En la próxima lección, profundizaremos en funciones de primera clase y orden superior, que son otro pilar importante de la programación funcional.

Aprendizajes de esta lección

  • Comprender el concepto de funciones puras y sus características principales: determinismo, ausencia de efectos secundarios y transparencia referencial.
  • Identificar correctamente diferentes tipos de efectos secundarios en código TypeScript y explicar por qué afectan a la pureza de las funciones.
  • Reconocer los beneficios prácticos de utilizar funciones puras, como mejor testabilidad, predictibilidad y seguridad en entornos concurrentes.
  • Aplicar técnicas básicas para transformar funciones impuras en versiones puras mediante la separación de responsabilidades.
  • Implementar operaciones inmutables en arrays y objetos para mantener la pureza funcional, aplicando los conocimientos de inmutabilidad de la lección anterior.

Completa TypeScript y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración